Inhaltsrichtlinie (CSP): mit Pruefsummen statt mit unsafe-inline

Fuenf Schutz-Kopfzeilen waren gesetzt, die wichtigste fehlte. Sie
entscheidet als einzige darueber, ob eingeschleuster Text zu
ausgefuehrtem Code wird oder sichtbarer Text bleibt.

WARUM NICHT DER BEQUEME WEG

Ueblich waere script-src 'self' 'unsafe-inline'. Eine Zeile, nichts geht
kaputt -- und der Schutz ist weg: Der Browser kann nicht unterscheiden,
ob ein Skript im Seitentext vom Entwickler stammt oder von einem
Angreifer. Das Ergebnis ist eine Kopfzeile, die gut aussieht und im
Ernstfall nichts tut.

Der Bestand liess den sauberen Weg zu: keine fremden Skriptquellen,
keine externen Schriften, ein Inline-Block je Seite. Von jedem Block
wird die Pruefsumme gebildet.

DIE PRUEFSUMMEN STEHEN BEWUSST NICHT IM CODE

Das waere hier eine Falle mit Ansage: Statische Dateien gehen per "git
pull" live, OHNE Neustart. Eine fest hinterlegte Summe waere nach der
naechsten Textaenderung falsch -- und die Seite wuerde ihr eigenes
Skript nicht mehr ausfuehren. Sichtbar erst im Browser des Besuchers,
nicht beim Deploy, und aussehend wie kaputtes JavaScript.

Deshalb liest die Middleware die Datei selbst und merkt sich das
Ergebnis, solange die Aenderungszeit gleich bleibt. Ein Test aendert
index.html im laufenden Betrieb und prueft, dass die Summe nachzieht
und die Seite weiterlaeuft.

WAS DER TEST GEFUNDEN HAT

Die erste Fassung haette die Startseite und stimmen.html beschaedigt:
Team-Fotos, Event des Jahres und die Stimmen kommen von der
postfach-Subdomain, img-src erlaubte nur 'self'. Der Deploy haette
Erfolg gemeldet, der Server waere gestartet -- und die Bilder waeren
weg gewesen. Gefunden, weil der Test alle 48 Seiten in einem echten
Browser oeffnet und mitschreibt, was blockiert wird.

Ein zweiter Fehlschlag lag am Test selbst: Er verlangte eine Pruefsumme
auf jeder Seite, auch auf denen ohne Inline-Block. Ein Test, der
Unmoegliches fordert, wird frueher oder spaeter abgeschaltet -- er
unterscheidet jetzt nach dem tatsaechlichen Inhalt der Datei.

DREI onclick-ATTRIBUTE ENTFERNT

Sie haetten 'unsafe-inline' erzwungen. Zweimal ein "Coming soon"-Knopf,
dessen onclick nur Klicks abfing -- ein <a> ohne href tut das von
selbst, ganz ohne Skript. Einmal ein Schliessen-Knopf im
Verwaltungsbereich, jetzt mit angehaengtem Zuhoerer.

GEGENPROBE

Ohne sie waere der Rest wertlos: Eine Richtlinie, die alles erlaubt,
blockiert auch nichts und besteht jede Pruefung. Der Test schleust
deshalb echten Code ein -- ein Inline-Skript und eines von fremder
Adresse -- und beide muessen scheitern.

style-src behaelt 'unsafe-inline': 587 style-Attribute im Bestand, deren
Umbau ein echtes Risiko fuers Aussehen waere, bei kleinem Gewinn. Ueber
Stile laesst sich verschleiern und ueberdecken, aber kein Code
ausfuehren. Bleibt als eigener Punkt auf der Liste.

15 Pruefungen gruen, 48 Seiten sauber, Portfolio-Suite unveraendert 35.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-26 14:05:42 +02:00
co-authored by Claude Opus 5
parent 094a69732b
commit 03a533f143
7 changed files with 392 additions and 4 deletions
+1 -1
View File
@@ -595,7 +595,7 @@
<h2 data-i18n="ab_vanvan_h2">Die Teddy-Kollektion — in Kooperation mit VanVan</h2>
<p data-i18n="ab_vanvan_text">Die DogiCrew 🩵 gehört Dogfather. Die Hasen-Teddys und die dazugehörigen Sammlerkollektionen entstehen jedoch in Zusammenarbeit mit VanVan — der rechten Hand von Dogfather, seiner wichtigsten Vertrauensperson und einem festen Bestandteil der Team-Dogi-Familie. Sie stehen für ihre besondere Rolle, den Zusammenhalt innerhalb der Community und die Verbindung zwischen Dogfather, VanVan und allen Menschen, die diesen gemeinsamen Weg dauerhaft unterstützen.</p>
<div style="display:flex;justify-content:flex-end;margin-top:1.4rem;">
<a class="btn-silver" href="#" aria-disabled="true" onclick="return false;"><span class="btn-silver-spark" aria-hidden="true">✨</span><span class="btn-silver-text" data-i18n="ab_kollektion_coming_soon">Coming soon…</span><span class="btn-silver-spark" aria-hidden="true">✨</span></a>
<a class="btn-silver" aria-disabled="true"><span class="btn-silver-spark" aria-hidden="true">✨</span><span class="btn-silver-text" data-i18n="ab_kollektion_coming_soon">Coming soon…</span><span class="btn-silver-spark" aria-hidden="true">✨</span></a>
</div>
</div>
</div>
+180
View File
@@ -0,0 +1,180 @@
/* =====================================================================
Prueft die Inhaltsrichtlinie (Content-Security-Policy).
Bei dieser Kopfzeile ist der gefaehrliche Fehler NICHT, dass sie zu
schwach ist -- sondern dass sie zu streng ist und die Seite lahmlegt.
Und zwar erst im Browser des Besuchers: Der Deploy meldet Erfolg, der
Server startet, die Seite laedt, und dann tut kein einziger Knopf mehr
etwas. Es sieht aus wie kaputtes JavaScript und hat mit JavaScript
nichts zu tun.
Deshalb wird hier JEDE Seite in einem echten Browser geoeffnet und
mitgeschrieben, was der Browser blockiert. Eine einzige blockierte
Meldung laesst den Test durchfallen.
Der zweite Teil ist die Gegenprobe: Eine Richtlinie, die alles
erlaubt, wuerde den ersten Teil muehelos bestehen. Deshalb wird
ausdruecklich geprueft, dass sie das Gefaehrliche auch WIRKLICH
verbietet -- mit eingeschleustem Code, der scheitern muss.
===================================================================== */
import { chromium } from "playwright";
import express from "./server/node_modules/express/index.js";
import { inhaltsrichtlinie } from "./server/inhaltsrichtlinie.js";
import fs from "fs";
import path from "path";
const WURZEL = process.cwd();
let ok = 0, fehl = 0;
const pruefe = (name, gut, info) => {
if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); }
};
const app = express();
app.use(inhaltsrichtlinie(WURZEL));
app.use(express.static(WURZEL, { index: "index.html" }));
const server = app.listen(4408);
await new Promise((f) => server.on("listening", f));
const browser = await chromium.launch();
/* Alle Seiten, die es gibt -- nicht nur eine Auswahl. Gerade die selten
besuchten faellt sonst niemandem auf. */
const seitenListe = [
...fs.readdirSync(WURZEL).filter((f) => f.endsWith(".html")).map((f) => "/" + f),
...fs.readdirSync(path.join(WURZEL, "webdesign"))
.filter((f) => f.endsWith(".html")).map((f) => "/webdesign/" + f),
];
console.log(`1. BLOCKIERT DIE RICHTLINIE ETWAS? (${seitenListe.length} Seiten)`);
const betroffen = [];
for (const pfad of seitenListe) {
const seite = await browser.newPage({ viewport: { width: 1280, height: 900 } });
const verstoesse = [];
seite.on("console", (m) => {
const t = m.text();
if (/Content Security Policy|Refused to (execute|load|apply)/i.test(t)) {
verstoesse.push(t.slice(0, 150));
}
});
await seite.goto("http://127.0.0.1:4408" + pfad, { waitUntil: "domcontentloaded" })
.catch(() => {});
await seite.waitForTimeout(700);
if (verstoesse.length) betroffen.push({ pfad, verstoesse });
await seite.close();
}
pruefe("keine Seite meldet einen Verstoss", betroffen.length === 0,
betroffen.length
? betroffen.slice(0, 3).map((b) => b.pfad + ": " + b.verstoesse[0]).join(" | ")
: seitenListe.length + " Seiten sauber");
if (betroffen.length) {
console.log("\n Betroffene Seiten im Einzelnen:");
for (const b of betroffen) {
console.log(" " + b.pfad);
for (const v of b.verstoesse.slice(0, 2)) console.log(" " + v);
}
}
console.log("\n2. STEHT UEBERALL EINE RICHTLINIE?");
/* Eine Pruefsumme kann nur dort stehen, wo es auch einen Inline-Block
gibt. Zuerst hat dieser Test sie ueberall verlangt und bei
portfolio.html und 404.html angeschlagen -- die haben schlicht keinen.
Ein Test, der Unmoegliches fordert, wird frueher oder spaeter
abgeschaltet; deshalb wird jetzt nach dem tatsaechlichen Inhalt der
Datei unterschieden. */
for (const pfad of ["/index.html", "/webdesign/portfolio.html", "/404.html",
"/webdesign/verwaltung.html"]) {
const a = await fetch("http://127.0.0.1:4408" + pfad);
const r = a.headers.get("content-security-policy") || "";
const quelle = fs.readFileSync(path.join(WURZEL, pfad), "utf8");
const hatInline = /<script(?![^>]*\ssrc=)[^>]*>[\s\S]*?<\/script>/i.test(quelle);
pruefe(` ${pfad}: Richtlinie gesetzt`,
r.includes("script-src 'self'") && r.includes("object-src 'none'"),
r ? r.slice(0, 46) + "…" : "FEHLT");
pruefe(` ${pfad}: ${hatInline ? "Pruefsumme vorhanden" : "keine noetig (kein Inline-Block)"}`,
hatInline ? r.includes("'sha256-") : !r.includes("'sha256-"));
}
/* Der Kniff, auf den es bei diesem Aufbau ankommt: Aendert sich die
HTML-Datei, muss die Pruefsumme SOFORT nachziehen. Sonst blockiert die
Richtlinie nach dem naechsten "git pull" das Skript der eigenen Seite
-- und zwar erst im Browser des Besuchers, nicht beim Deploy. */
console.log("\n2b. ZIEHT DIE PRUEFSUMME BEI EINER AENDERUNG NACH?");
const pruefDatei = path.join(WURZEL, "index.html");
const vorher = (await fetch("http://127.0.0.1:4408/index.html"))
.headers.get("content-security-policy");
const inhaltVorher = fs.readFileSync(pruefDatei, "utf8");
try {
/* Eine Zeile im Inline-Block aendern -- so, wie es eine echte
Textaenderung taete. */
fs.writeFileSync(pruefDatei,
inhaltVorher.replace("<script>", "<script>\n/* Pruefzeile */"), "utf8");
const nachher = (await fetch("http://127.0.0.1:4408/index.html"))
.headers.get("content-security-policy");
pruefe(" nach der Aenderung eine ANDERE Pruefsumme",
vorher !== nachher && (nachher || "").includes("'sha256-"),
vorher === nachher ? "unveraendert — die Seite waere jetzt kaputt" : "nachgezogen");
/* Und die neue Summe muss zum neuen Inhalt passen, nicht nur anders
sein. Das prueft ein Browser am ehrlichsten. */
const s2 = await browser.newPage();
const v2 = [];
s2.on("console", (m) => { if (/Content Security Policy|Refused to/i.test(m.text())) v2.push(m.text()); });
await s2.goto("http://127.0.0.1:4408/index.html", { waitUntil: "domcontentloaded" });
await s2.waitForTimeout(600);
pruefe(" die geaenderte Seite laeuft weiterhin", v2.length === 0, v2[0]?.slice(0, 80) || "sauber");
await s2.close();
} finally {
fs.writeFileSync(pruefDatei, inhaltVorher, "utf8");
}
const a2 = await fetch("http://127.0.0.1:4408/assets/css/main.css");
pruefe(" auch fuer Nicht-Seiten (CSS)",
(a2.headers.get("content-security-policy") || "").includes("default-src 'none'"));
console.log("\n3. GEGENPROBE: VERBIETET SIE AUCH WIRKLICH ETWAS?");
/* Ohne diesen Abschnitt waere Teil 1 wertlos: Eine Richtlinie, die
alles erlaubt, blockiert auch nichts und bestuende jede Pruefung. */
const wache = await browser.newPage();
const geblockt = [];
wache.on("console", (m) => {
if (/Content Security Policy|Refused to/i.test(m.text())) geblockt.push(m.text().slice(0, 90));
});
await wache.goto("http://127.0.0.1:4408/index.html", { waitUntil: "domcontentloaded" });
await wache.waitForTimeout(500);
/* Genau der Angriff, gegen den die Richtlinie gedacht ist: ein Skript,
das in den Seitentext geraet. Es MUSS scheitern. */
const eingeschleust = await wache.evaluate(() => {
window.__boesartig = false;
const s = document.createElement("script");
s.textContent = "window.__boesartig = true;";
document.body.appendChild(s);
return window.__boesartig;
});
pruefe("eingeschleustes Inline-Skript wird NICHT ausgefuehrt", eingeschleust === false,
eingeschleust ? "es lief durch — die Richtlinie wirkt nicht!" : "blockiert");
const fremd = await wache.evaluate(async () => {
const s = document.createElement("script");
s.src = "https://example.com/boese.js";
const geladen = new Promise((f) => { s.onload = () => f("geladen"); s.onerror = () => f("blockiert"); });
document.body.appendChild(s);
return await geladen;
});
pruefe("Skript von fremder Adresse wird NICHT geladen", fremd === "blockiert", fremd);
pruefe("der Browser hat die Blockade auch gemeldet", geblockt.length > 0,
geblockt[0] || "keine Meldung");
await browser.close();
server.close();
console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`);
process.exit(fehl ? 1 : 0);
+12
View File
@@ -12,6 +12,7 @@ import { fileURLToPath } from "node:url";
import { gateMiddleware } from "./gate.js";
import { webdesignGate } from "./webdesign-gate.js";
import { inhaltsrichtlinie } from "./inhaltsrichtlinie.js";
const __dirname = dirname(fileURLToPath(import.meta.url));
const SITE_DIR = join(__dirname, "..");
@@ -34,6 +35,17 @@ app.use((req, res, next) => {
next();
});
/* Inhaltsrichtlinie (26.08.2026). Steht bewusst HIER -- vor jeder
Schranke und vor express.static: Auch die Zugangswand und die
404-Seite sollen sie bekommen, denn gerade dort werden Eingaben
entgegengenommen.
Warum die Pruefsummen nicht fest im Code stehen, ist im Kopf von
inhaltsrichtlinie.js begruendet -- kurz: Statische Dateien gehen hier
ohne Neustart live, feste Summen waeren nach der naechsten
Textaenderung falsch und wuerden die Seite lahmlegen. */
app.use(inhaltsrichtlinie(SITE_DIR));
app.use(cookieParser());
app.use(express.json({ limit: "1mb" }));
app.use(gateMiddleware);
+181
View File
@@ -0,0 +1,181 @@
/* =====================================================================
inhaltsrichtlinie.js — Content-Security-Policy, die sich selbst pflegt
WAS SIE VERHINDERT
Gelingt es jemandem, eigenen Text in eine Seite einzuschleusen -- über
ein Formular, eine Adresszeile, einen gespeicherten Namen --, dann
entscheidet allein diese Kopfzeile darüber, ob daraus ausgeführter
Code wird oder nur sichtbarer Text.
WARUM NICHT EINFACH 'unsafe-inline'
Das ist der übliche Weg, und er ist bequem: eine Zeile, fertig, nichts
geht kaputt. Er hebt aber genau den Schutz auf, um den es geht. Der
Browser kann nicht unterscheiden, ob ein Skript im Seitentext vom
Entwickler stammt oder von einem Angreifer -- inline ist inline. Wer
'unsafe-inline' erlaubt, hat eine Kopfzeile, die gut aussieht und im
Ernstfall nichts tut.
Diese Seite hat KEINE fremden Skriptquellen und genau einen
Inline-Block je Seite. Damit ist der saubere Weg realistisch: Von
jedem Block wird die Prüfsumme gebildet und in die Regel geschrieben.
Der Browser führt dann genau diese Blöcke aus und sonst nichts.
⚠️ WARUM DIE PRÜFSUMMEN NICHT FEST IM CODE STEHEN
Das wäre der naheliegende Weg -- und hier eine Falle mit Ansage:
Statische Dateien gehen bei diesem Projekt per "git pull" live, OHNE
Neustart (siehe DEPLOY.md). Eine fest hinterlegte Prüfsumme würde nach
der nächsten Textänderung nicht mehr zum ausgelieferten HTML passen --
und dann führt die Seite ihr eigenes Skript nicht mehr aus. Der Fehler
erschiene erst im Browser, nicht beim Deploy, und sähe aus wie ein
kaputtes JavaScript.
Deshalb liest diese Datei die HTML-Datei selbst und merkt sich das
Ergebnis, solange sich deren Änderungszeit nicht ändert. Ein "git
pull" wirkt damit sofort, ganz ohne Zutun.
WAS BEWUSST ERLAUBT BLEIBT
style-src erlaubt 'unsafe-inline'. Im Bestand stehen 587 style-Attribute
im HTML; sie alle zu ersetzen wäre eine große Umbauaktion mit echtem
Risiko für das Aussehen. Der Gewinn wäre klein: Über Stile lassen sich
Inhalte verschleiern oder überdecken, aber kein Code ausführen. Der
Unterschied zu script-src ist also nicht graduell, sondern
grundsätzlich. Bleibt als eigener Punkt auf der Liste.
===================================================================== */
import crypto from "crypto";
import { readFileSync, statSync } from "node:fs";
import { join, normalize } from "node:path";
/* Gemerkte Prüfsummen je Datei, zusammen mit der Änderungszeit, aus der
sie stammen. Ändert sich die Datei, wird neu gerechnet. */
const merker = new Map();
/* Findet alle Inline-Skriptblöcke und bildet je Block die Prüfsumme.
Wichtig: Gerechnet wird über den Inhalt EXAKT so, wie er zwischen den
Tags steht -- kein Trimmen, keine Umwandlung von Zeilenenden. Der
Browser rechnet über dieselben Bytes; schon ein entferntes Leerzeichen
ergibt eine andere Summe und damit ein blockiertes Skript. */
function summenBilden(html) {
const summen = [];
/* Nur Blöcke OHNE src. Ein <script src="..."> hat keinen Inhalt, der
zu prüfen wäre -- der ist über 'self' abgedeckt. */
const muster = /<script(?![^>]*\ssrc=)[^>]*>([\s\S]*?)<\/script>/gi;
let treffer;
while ((treffer = muster.exec(html)) !== null) {
const inhalt = treffer[1];
if (!inhalt) continue;
const summe = crypto.createHash("sha256").update(inhalt, "utf8").digest("base64");
summen.push(`'sha256-${summe}'`);
}
return summen;
}
function summenFuer(datei) {
let stand;
try {
stand = statSync(datei);
} catch {
return null;
}
const schluessel = datei;
const gemerkt = merker.get(schluessel);
if (gemerkt && gemerkt.zeit === stand.mtimeMs) return gemerkt.summen;
let summen;
try {
summen = summenBilden(readFileSync(datei, "utf8"));
} catch {
return null;
}
merker.set(schluessel, { zeit: stand.mtimeMs, summen });
return summen;
}
/* Aus einer angefragten Adresse die HTML-Datei bestimmen, die
express.static gleich ausliefern wird. */
function dateiZuPfad(wurzel, url) {
const rein = decodeURIComponent(url.split("?")[0].split("#")[0]);
let pfad = rein.endsWith("/") ? rein + "index.html" : rein;
if (!/\.html?$/i.test(pfad)) {
/* Adressen ohne Endung liefert express.static nicht als HTML aus --
außer es ist ein Verzeichnis. Das behandelt der Fall oben. */
return null;
}
const voll = normalize(join(wurzel, pfad));
/* Sicherheitsnetz gegen "../" in der Adresse: Was außerhalb der Wurzel
landet, wird nicht angefasst. */
if (!voll.startsWith(normalize(wurzel))) return null;
return voll;
}
export function inhaltsrichtlinie(wurzel) {
return function (req, res, next) {
const datei = dateiZuPfad(wurzel, req.path === "/" ? "/index.html" : req.path);
/* Für alles, was keine Seite ist (Bilder, CSS, Schriften, API),
genügt eine knappe Regel. Sie kostet nichts und schadet nie. */
if (!datei) {
res.setHeader("Content-Security-Policy",
"default-src 'none'; frame-ancestors 'self'; base-uri 'none'");
return next();
}
const summen = summenFuer(datei);
/* Ließ sich die Datei nicht lesen, wird KEINE strenge Regel gesetzt.
Das ist Absicht: Eine Regel ohne die passenden Prüfsummen würde
jedes Skript der Seite blockieren. Eine unbenutzbare Seite ist
schlimmer als eine ungeschützte -- und dieser Fall tritt nur ein,
wenn ohnehin etwas grundlegend nicht stimmt. */
if (!summen) return next();
const skript = ["'self'", ...summen].join(" ");
res.setHeader("Content-Security-Policy", [
"default-src 'self'",
`script-src ${skript}`,
/* Siehe Kopf der Datei: bewusste Ausnahme, eigener Punkt auf der Liste. */
"style-src 'self' 'unsafe-inline'",
/* data: für eingebettete Kleingrafiken, blob: für selbst erzeugte
Vorschauen beim Hochladen.
Die postfach-Subdomain steht hier, weil von dort ALLE selbst
gepflegten Bilder kommen: Team-Fotos, das Event des Jahres, die
Stimmen. Ohne diesen Eintrag wären auf der Startseite und auf
stimmen.html schlicht die Bilder weg -- gemeldet vom Test, der
die Seiten in einem echten Browser öffnet. Genau dafür gibt es
ihn: Der Deploy hätte Erfolg gemeldet, der Server wäre
gestartet, und der Schaden wäre erst jemandem aufgefallen, der
die Seite ansieht. */
"img-src 'self' data: blob: https://postfach.dogfather-universe.com",
"font-src 'self'",
/* Die Seite spricht mit dem eigenen Server und mit dem internen
Bereich unter postfach. -- mehr nicht. Ein eingeschleustes
Skript könnte damit keine Daten nach außen schicken. */
"connect-src 'self' https://postfach.dogfather-universe.com",
/* Kein Flash, keine Java-Applets, keine eingebetteten Objekte. */
"object-src 'none'",
/* Verhindert, dass ein eingeschleustes <base> alle relativen
Adressen der Seite auf einen fremden Server umbiegt. */
"base-uri 'self'",
/* Verhindert, dass ein Formular seine Eingaben woandershin
schickt -- der klassische Weg, ein Passwort abzugreifen, ohne
eine einzige Zeile Code auszuführen. */
"form-action 'self'",
/* Ersetzt X-Frame-Options; wirkt gegen Klickfallen. */
"frame-ancestors 'self'",
"frame-src 'none'",
"worker-src 'self'",
/* Hebt gemischte Inhalte auf HTTPS, statt sie zu blockieren. */
"upgrade-insecure-requests",
].join("; "));
next();
};
}
+1 -1
View File
@@ -58,7 +58,7 @@
<p data-i18n="vv_wer_tiktok">Auf TikTok findet ihr mich unter:</p>
<a class="btn-tiktok" href="https://www.tiktok.com/@o.o.vanessa.o.o" target="_blank" rel="noopener" style="margin-bottom:.7rem;"><span class="btn-tiktok-note" aria-hidden="true">♪</span><span class="btn-tiktok-text">TikTok</span></a>
<br />
<a class="btn-silver" href="#" aria-disabled="true" onclick="return false;" style="margin-bottom:1rem;"><span class="btn-silver-spark" aria-hidden="true">✨</span><span class="btn-silver-text" data-i18n="vv_coming_soon">Coming soon…</span><span class="btn-silver-spark" aria-hidden="true">✨</span></a>
<a class="btn-silver" aria-disabled="true" style="margin-bottom:1rem;"><span class="btn-silver-spark" aria-hidden="true">✨</span><span class="btn-silver-text" data-i18n="vv_coming_soon">Coming soon…</span><span class="btn-silver-spark" aria-hidden="true">✨</span></a>
<p data-i18n="vv_wer_1" style="margin-top:1.4rem;">Ich bin ein Mensch, der mit dem Herzen denkt, unglaublich loyal ist und für die Menschen kämpft, die ihm wichtig sind.</p>
<p data-i18n="vv_wer_2">Wenn ich zu jemandem Vertrauen aufgebaut habe, gibt es für mich kein „halb“. Dann bin ich da – unabhängig davon, ob gerade gute oder schwierige Zeiten herrschen.</p>
<p data-i18n="vv_wer_3">Freundschaft, Zusammenhalt und Ehrlichkeit bedeuten mir mehr als alles andere. Für mich zählen keine Fassaden, sondern der Charakter eines Menschen. Wenn ich jemanden unterstütze, dann nicht, weil es von mir erwartet wird, sondern weil ich aus voller Überzeugung hinter diesem Menschen stehe.</p>
+1 -1
View File
@@ -34,7 +34,7 @@
Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den
Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten
Stand fest. */
const CACHE_NAME = "dogfather-webdesign-v54";
const CACHE_NAME = "dogfather-webdesign-v55";
/* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE
HTML-Datei in dieser Liste. */
+16 -1
View File
@@ -3884,8 +3884,23 @@
});
} catch (fehler) {
/* Der Schließen-Knopf hatte hier ein onclick-Attribut. Das ist die
einzige Stelle im Webdesign-Bereich, die noch eines hatte -- und
genau solche Attribute zwingen eine Inhaltsrichtlinie dazu,
'unsafe-inline' zu erlauben. Damit wäre ihr Hauptzweck dahin:
Sie soll ja verhindern, dass eingeschleuster Code im Seitentext
ausgeführt wird, und kann inline nicht von inline unterscheiden.
Der Knopf tut jetzt dasselbe über einen angehängten Zuhörer. */
blatt.innerHTML = '<div class="vw-leer-hinweis">' + s(fehler.message || "Konnte nicht geladen werden.") +
'<br /><br /><button class="wd-btn wd-btn--still wd-btn--klein" type="button" onclick="this.closest(\'.vw-detail\').classList.remove(\'vw-auf\')">Schließen</button></div>';
'<br /><br /><button class="wd-btn wd-btn--still wd-btn--klein vw-blatt-zu" type="button">Schließen</button></div>';
var zuKnopf = blatt.querySelector(".vw-blatt-zu");
if (zuKnopf) {
zuKnopf.addEventListener("click", function () {
var d = zuKnopf.closest(".vw-detail");
if (d) d.classList.remove("vw-auf");
});
}
}
}