Files
dogfather-universe/pruef-inhaltsrichtlinie.mjs
DogFatherGitandClaude Opus 5 77b395e2b4 Test: laesst den Service Worker jetzt wirklich arbeiten
Die Luecke, durch die der Fehler von heute bis zum Benutzer
durchgerutscht ist.

Der Test hat 48 Seiten in einem echten Browser geoeffnet und nichts
bemerkt -- weil er den Service Worker nie hat arbeiten lassen. Er war
gruen und wertlos zugleich: Der entscheidende Weg wurde nicht
begangen.

Neu geprueft wird jetzt:
  - der Service Worker meldet sich an und wird aktiv
  - er darf tatsaechlich etwas abrufen (das war der kaputte Punkt)
  - dabei entsteht kein Richtlinien-Verstoss
  - sw.js traegt selbst KEINE Richtlinie, denn sie wuerde zu SEINER

Ausserdem umgedreht: Ein Test verlangte fuer Nicht-Seiten ausdruecklich
"default-src 'none'" -- und sicherte damit genau den Fehler ab, der die
App lahmlegte. Er haette den naechsten Versuch, es richtig zu machen,
als Fehler gemeldet. Jetzt wird geprueft, dass CSS, JavaScript, Bilder
und der Service Worker KEINE Richtlinie bekommen.

22 Pruefungen gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-26 14:47:10 +02:00

256 lines
11 KiB
JavaScript

/* =====================================================================
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");
}
/* Umgedreht am 26.08.2026. Vorher verlangte dieser Test fuer
Nicht-Seiten ausdruecklich "default-src 'none'" -- und hat damit
genau den Fehler abgesichert, der die App lahmgelegt hat.
Eine Richtlinie steuert, was ein DOKUMENT nachladen darf. Ein
Stylesheet laedt nichts nach, dort ist sie wirkungslos. Bei einem
Service-Worker-Skript ist sie das Gegenteil von wirkungslos: Sie
wird zur Richtlinie des Workers und kann ihn stilllegen.
Deshalb gilt jetzt: Richtlinie nur fuer HTML. Ein Test, der das
Gegenteil verlangt, haette den naechsten Versuch, es richtig zu
machen, als Fehler gemeldet. */
for (const pfad of ["/assets/css/main.css", "/assets/js/wd-core.js",
"/webdesign/sw.js", "/assets/img/favicon.png"]) {
const a = await fetch("http://127.0.0.1:4408" + pfad);
const r = a.headers.get("content-security-policy");
pruefe(` ${pfad}: keine Richtlinie`, !r, r ? "hat eine: " + r.slice(0, 40) : "keine");
}
/* =====================================================================
2c. DER SERVICE WORKER MUSS ARBEITEN KOENNEN
⚠️ Dieser Abschnitt fehlte, und genau durch die Luecke ist am
26.08.2026 ein Fehler bis zum Benutzer durchgerutscht:
Die erste Fassung setzte fuer alles, was keine Seite ist,
"default-src 'none'" -- mit dem Gedanken "kostet nichts, schadet
nie". Ein Service Worker uebernimmt aber die Richtlinie, die beim
Herunterladen SEINER EIGENEN Skriptdatei galt. Mit 'none' darf er
nichts mehr abrufen; jede Anfrage scheitert, und er zeigt seine
Offline-Seite. Fuer den Benutzer sah das aus wie ein Netzausfall --
bei laufendem Server und einer Seite, die im Browser tadellos lud.
Der Test von damals hat alle 48 Seiten geoeffnet und nichts bemerkt,
weil er den Service Worker nie hat arbeiten lassen. Eine Pruefung,
die den entscheidenden Weg auslaesst, ist gruen und wertlos zugleich.
===================================================================== */
console.log("\n2c. KANN DER SERVICE WORKER ARBEITEN?");
{
const kontext = await browser.newContext({ serviceWorkers: "allow" });
const sw = await kontext.newPage();
const verstoesse = [];
sw.on("console", (m) => {
if (/Content Security Policy|Refused to/i.test(m.text())) verstoesse.push(m.text().slice(0, 110));
});
await sw.goto("http://127.0.0.1:4408/webdesign/zugang.html", { waitUntil: "networkidle" });
await sw.waitForTimeout(3500);
const stand = await sw.evaluate(async () => {
const r = await navigator.serviceWorker.getRegistration("/webdesign/");
return { da: !!r, aktiv: !!(r && r.active) };
});
pruefe(" Service Worker meldet sich an", stand.da, stand.aktiv ? "aktiv" : "noch nicht aktiv");
/* Der Kern: Ein Service Worker, der nichts holen darf, ist genau der
Fehler von damals -- und von aussen nicht zu sehen. */
const holen = await sw.evaluate(async () => {
try {
const a = await fetch("/assets/css/webdesign.css", { cache: "no-store" });
return { ok: a.ok, laenge: (await a.text()).length };
} catch (e) {
return { ok: false, fehler: String(e).slice(0, 70) };
}
});
pruefe(" er darf auch etwas abrufen", holen.ok,
holen.ok ? holen.laenge + " Bytes" : holen.fehler);
pruefe(" dabei kein Richtlinien-Verstoss", verstoesse.length === 0, verstoesse[0] || "sauber");
/* Und die Skriptdatei selbst darf keine Richtlinie mitbekommen -- sie
wuerde sonst zur Richtlinie des Workers. */
const kopf = (await fetch("http://127.0.0.1:4408/webdesign/sw.js"))
.headers.get("content-security-policy");
pruefe(" sw.js traegt selbst KEINE Richtlinie", !kopf, kopf || "keine — richtig so");
await kontext.close();
}
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);