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]>
256 lines
11 KiB
JavaScript
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);
|