Kasse und Google-Anmeldung in der Inhaltsrichtlinie freigegeben

Fortsetzung des Musik-Fundes: PayPal und Google haetten an derselben
Stelle still versagt, sobald Dogi seine Zugangsdaten eintraegt -- leerer
Bereich statt Bezahlknopf, ohne Fehlermeldung, ohne Protokolleintrag.

Vorgehen bewusst gemessen statt geraten: Erst die Angaben der Anbieter
(PayPal "Best Practices", Google CSP-Abschnitt der Setup-Anleitung),
dann im echten Browser mit PayPals offizieller Testkennung
"client-id=test" nachgemessen und die Verstoesse ueber das Ereignis
securitypolicyviolation eingesammelt. Die Messung hat zwei Dinge
gefunden, die in keiner Anleitung standen:

- www.sandbox.paypal.com (frame-src + connect-src). Beim Einrichten
  testet man mit Sandbox-Zugangsdaten; ohne diesen Eintrag haette die
  Kasse in genau dieser Phase nicht abgeschlossen werden koennen.
- accounts.google.com/gsi/style (style-src). 'unsafe-inline' deckt das
  NICHT ab -- es erlaubt nur Stile im Dokument, keine nachgeladene
  Stilvorlage. Der Anmelde-Knopf waere unformatiert erschienen.

Bewusst einzelne Adressen statt PayPals vorgeschlagener Platzhalter
(*.paypal.com): Was die Messung nicht gebraucht hat, steht nicht drin.
Skripte bleiben ohne 'unsafe-inline' -- die Pruefsummen-Loesung fuer die
eigenen Inline-Bloecke bleibt unangetastet, und ein zusaetzliches
'unsafe-inline' waere neben Pruefsummen ohnehin wirkungslos.

Neuer Test pruef-kasse.mjs (15 Pruefungen): laedt das echte PayPal-SDK,
baut die Bezahlknoepfe wirklich auf, rendert den echten Google-Knopf und
verlangt NULL Verstoesse. Bestandstest pruef-inhaltsrichtlinie.mjs (22)
und pruef-musik.mjs (17) bleiben gruen -- inklusive der Gegenprobe, dass
eingeschleuste Skripte weiterhin blockiert werden.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
2026-08-27 15:11:27 +02:00
co-authored by Claude Opus 5
parent 87db17704a
commit 8fa15c04f1
4 changed files with 353 additions and 6 deletions
+185
View File
@@ -0,0 +1,185 @@
/* =====================================================================
Kasse & Anmeldung gegen die Inhaltsrichtlinie (27.08.2026)
WORUM ES GEHT
Auf abonnieren.html kommen zwei Dinge von fremden Servern: die
PayPal-Kasse und die Google-Anmeldung. Beide werden erst NACH einer
Nutzerhandlung nachgeladen (Klick bzw. Anmeldung) und beide sind
derzeit nicht scharf geschaltet -- googleClientId und paypalClientId
stehen auf null. Ein Fehler dort fällt deshalb heute NICHT auf. Er
fällt erst an dem Tag auf, an dem Dogi seine echten Zugangsdaten
einträgt: Dann stünde an der Kasse ein leerer Bereich, ohne
Fehlermeldung, ohne Eintrag im Server-Protokoll.
Genau dieses Muster hat schon einmal zugeschlagen (Musik-Knopf,
26.-27.08.2026). Deshalb wird hier vorgezogen geprüft, statt zu warten.
WIE ECHT DIESER TEST IST
- Er startet den ECHTEN server/index.js, damit die echte Kopfzeile
ausgeliefert wird. Ein Datei-Server erzeugt sie gar nicht.
- Er lädt das ECHTE PayPal-SDK mit deren offizieller Testkennung
("client-id=test") und baut die Knöpfe wirklich auf.
- Er lädt die ECHTE Google-Bibliothek und rendert den Knopf wirklich.
- Verstöße werden nicht aus Konsolentext geraten, sondern über das
Ereignis "securitypolicyviolation" gezählt -- dort stehen Richtlinie
und blockierte Adresse maschinenlesbar drin.
Kein echter Zugangscode und kein echtes Geld sind beteiligt.
===================================================================== */
import { chromium } from "playwright";
import { spawn } from "node:child_process";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const HIER = dirname(fileURLToPath(import.meta.url));
const PORT = 4435;
const BASIS = `http://127.0.0.1:${PORT}`;
let ok = 0, fehl = 0;
const funde = [];
const pruefe = (name, gut, info) => {
if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); }
else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); funde.push(name + " — " + info); }
};
const server = spawn(process.execPath, ["index.js"], {
cwd: join(HIER, "server"),
env: { ...process.env, PORT: String(PORT) },
stdio: "ignore",
});
const beenden = (code) => { try { server.kill(); } catch { /* schon weg */ } process.exit(code); };
/* Notbremse: PayPals render() wartet ohne echten Bezahlvorgang unbegrenzt.
Ohne diese Grenze bliebe der Test im Zweifel ewig hängen. */
const notbremse = setTimeout(() => { console.log("\nZEIT ABGELAUFEN — Testlauf abgebrochen."); beenden(1); }, 180000);
notbremse.unref();
async function warteAufServer() {
for (let i = 0; i < 40; i++) {
try {
const res = await fetch(BASIS + "/abonnieren.html");
if (res.ok) return res;
} catch { /* noch nicht oben */ }
await new Promise((r) => setTimeout(r, 250));
}
throw new Error("Server ist nicht hochgekommen");
}
(async () => {
const antwort = await warteAufServer();
const richtlinie = antwort.headers.get("content-security-policy") || "";
const regel = (name) => (richtlinie.split(";").find((t) => t.trim().startsWith(name + " ")) || "").trim();
console.log("\n== Was die Kopfzeile verspricht ==");
pruefe("script-src kennt PayPal", /www\.paypal\.com/.test(regel("script-src")), regel("script-src").slice(0, 120));
pruefe("script-src kennt Google", /accounts\.google\.com\/gsi\/client/.test(regel("script-src")), "");
pruefe("frame-src kennt PayPal (Betrieb + Übung)", /www\.paypal\.com/.test(regel("frame-src")) && /sandbox\.paypal\.com/.test(regel("frame-src")), regel("frame-src"));
pruefe("frame-src kennt Google", /accounts\.google\.com\/gsi\//.test(regel("frame-src")), "");
pruefe("style-src kennt Googles Stilvorlage", /accounts\.google\.com\/gsi\/style/.test(regel("style-src")), regel("style-src"));
pruefe("Musik bleibt erlaubt", /open\.spotify\.com/.test(regel("frame-src")), "");
pruefe("Kein Sternchen in frame-src", !/\*/.test(regel("frame-src")), regel("frame-src"));
pruefe("Kein 'unsafe-inline' bei Skripten", !/unsafe-inline/.test(regel("script-src")), "");
const browser = await chromium.launch();
const seite = await browser.newPage({ viewport: { width: 1280, height: 900 } });
await seite.goto(BASIS + "/abonnieren.html", { waitUntil: "domcontentloaded" });
await seite.evaluate(() => {
window.__verstoesse = [];
document.addEventListener("securitypolicyviolation", (e) =>
window.__verstoesse.push({ regel: e.violatedDirective, blockiert: e.blockedURI || "(inline)" })
);
});
/* ---------- PayPal ---------- */
console.log("\n== PayPal-Kasse (offizielle Testkennung) ==");
const paypal = await seite.evaluate(async () => {
const geladen = await new Promise((res) => {
const sk = document.createElement("script");
sk.src = "https://www.paypal.com/sdk/js?client-id=test&currency=EUR";
sk.onload = () => res(true);
sk.onerror = () => res(false);
document.body.appendChild(sk);
setTimeout(() => res(false), 20000);
});
if (!geladen || !window.paypal) return { geladen, knoepfe: false };
const ziel = document.createElement("div");
ziel.id = "kassen-test";
ziel.style.width = "320px";
document.body.appendChild(ziel);
/* Absichtlich NICHT abwarten: render() loest ohne echten Bezahlvorgang
nie auf. Es geht nur darum, ob der Rahmen entstehen darf. */
try { window.paypal.Buttons({}).render("#kassen-test").catch(() => {}); } catch { /* egal */ }
await new Promise((r) => setTimeout(r, 9000));
const rahmen = ziel.querySelector("iframe");
return { geladen, knoepfe: !!rahmen, hoehe: rahmen ? Math.round(rahmen.getBoundingClientRect().height) : 0 };
});
pruefe("PayPal-SDK wird geladen", paypal.geladen, paypal.geladen ? "geladen" : "BLOCKIERT");
pruefe("Bezahlknöpfe werden aufgebaut", paypal.knoepfe, paypal.knoepfe ? paypal.hoehe + "px hoch" : "kein Rahmen entstanden");
/* ---------- Google ---------- */
console.log("\n== Google-Anmeldung ==");
const google = await seite.evaluate(async () => {
const geladen = await new Promise((res) => {
const sk = document.createElement("script");
sk.src = "https://accounts.google.com/gsi/client";
sk.onload = () => res(true);
sk.onerror = () => res(false);
document.head.appendChild(sk);
setTimeout(() => res(false), 20000);
});
if (!geladen || !window.google) return { geladen, knopf: false };
const ziel = document.createElement("div");
ziel.id = "anmelde-test";
document.body.appendChild(ziel);
try {
window.google.accounts.id.initialize({
client_id: "000000000000-testtesttesttesttesttest.apps.googleusercontent.com",
callback: () => {},
});
window.google.accounts.id.renderButton(ziel, { type: "standard" });
} catch { return { geladen, knopf: false }; }
await new Promise((r) => setTimeout(r, 5000));
return { geladen, knopf: ziel.children.length > 0 };
});
pruefe("Google-Bibliothek wird geladen", google.geladen, google.geladen ? "geladen" : "BLOCKIERT");
pruefe("Anmelde-Knopf wird aufgebaut", google.knopf, google.knopf ? "gerendert" : "leer geblieben");
const googleRahmen = seite.frames().filter((f) => f.url().startsWith("https://accounts.google.com/gsi/")).length;
pruefe("Googles Rahmen darf laden", googleRahmen >= 1, googleRahmen + " Rahmen");
/* ---------- Das eigentliche Urteil ---------- */
console.log("\n== Verstöße gegen die Inhaltsrichtlinie ==");
const verstoesse = await seite.evaluate(() => window.__verstoesse);
const gezaehlt = {};
for (const v of verstoesse) {
const schluessel = v.regel + " <- " + v.blockiert.split("?")[0];
gezaehlt[schluessel] = (gezaehlt[schluessel] || 0) + 1;
}
Object.entries(gezaehlt).forEach(([k, n]) => console.log(" " + n + "x " + k));
pruefe("Kein einziger Verstoß", verstoesse.length === 0, verstoesse.length + " Verstöße");
const fehlerRahmen = seite.frames().filter((f) => f.url().startsWith("chrome-error://")).length;
pruefe("Kein Rahmen endet als Fehlerseite", fehlerRahmen === 0, fehlerRahmen + " Fehlerseiten");
/* Bilder von genau den beiden Testelementen.
WICHTIG: Die Elemente werden dafür NICHT im Seitenbaum verschoben.
PayPal baut seine Knöpfe wieder ab, sobald ihr Rahmen umgehängt wird
— ein Bild davon zeigte dann fälschlich eine leere Kasse, obwohl sie
kurz zuvor sauber stand. */
for (const [wahl, datei] of [["#kassen-test", "bild-kasse-paypal.png"], ["#anmelde-test", "bild-kasse-google.png"]]) {
try {
await seite.locator(wahl).scrollIntoViewIfNeeded();
await seite.waitForTimeout(600);
await seite.locator(wahl).screenshot({ path: datei });
} catch { /* Bild ist Beiwerk, kein Prüfkriterium */ }
}
await browser.close();
clearTimeout(notbremse);
console.log("\n===== " + ok + " ok, " + fehl + " Fehler =====");
if (funde.length) { console.log("\nOffene Punkte:"); funde.forEach((f) => console.log(" - " + f)); }
beenden(fehl ? 1 : 0);
})().catch((e) => {
console.error("Testlauf abgebrochen:", e.message);
beenden(1);
});