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:
+1
-1
@@ -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>
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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
@@ -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
@@ -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. */
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user