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]>
186 lines
9.0 KiB
JavaScript
186 lines
9.0 KiB
JavaScript
/* =====================================================================
|
|
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¤cy=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);
|
|
});
|