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
+63 -6
View File
@@ -50,6 +50,61 @@ import crypto from "crypto";
import { readFileSync, statSync } from "node:fs";
import { join, normalize } from "node:path";
/* =====================================================================
FREMDE QUELLEN — hier stehen sie namentlich, nicht verstreut
Drei Dinge auf dieser Seite kommen nicht vom eigenen Server: die
Spotify-Player im Musik-Feld, die PayPal-Kasse und die
Google-Anmeldung auf abonnieren.html.
Warum sie hier zusammenstehen: Wer eine Quelle vergisst, merkt es
NICHT beim Deploy und nicht im Server-Protokoll. Der Browser wirft
das Element still weg -- genau so ist am 26.08.2026 der Musik-Knopf
ausgefallen und erst am 27.08. aufgefallen. Eine Liste an einer
Stelle, mit Begründung je Eintrag, ist die einzige Stelle, an der man
beim nächsten Mal nachsieht.
Ausgewählt wurde nach den Angaben der Anbieter (PayPal: "Best
Practices" der JS-SDK-Doku, Google: CSP-Abschnitt der Setup-Anleitung)
und danach mit pruef-kasse.mjs im echten Browser nachgemessen: Die
Kasse wird dort mit PayPals offizieller Testkennung wirklich
aufgebaut, die Anmeldung wirklich gerendert. Was dabei nicht gebraucht
wurde, steht hier auch nicht -- deshalb einzelne Adressen statt der
von PayPal vorgeschlagenen Platzhalter *.paypal.com.
===================================================================== */
/* Spotify: die zwei Player im Musik-Feld (main.js, jede Seite). */
const SPOTIFY = "https://open.spotify.com";
/* PayPal: das SDK selbst liegt auf www.paypal.com, Bilder/Schriften der
Knöpfe auf www.paypalobjects.com. Der Bezahlvorgang läuft anschließend
in einem Rahmen von www.paypal.com.
⚠️ Die Übungs-Umgebung (www.sandbox.paypal.com) steht bewusst MIT
drin, obwohl im Betrieb nur www.paypal.com gebraucht wird. Grund:
Beim Einrichten testet man zuerst mit Sandbox-Zugangsdaten -- das SDK
spricht dann ausschließlich mit der Sandbox-Adresse. Ohne diesen
Eintrag hätte Dogi beim ersten Testkauf eine Kasse gesehen, die sich
nicht abschließen lässt, ohne jeden Hinweis auf den Grund. Genau
dieser Verstoß ist bei der Messung mit PayPals Testkennung
aufgetreten -- geraten hätte man ihn nicht. */
const PAYPAL_SKRIPT = "https://www.paypal.com https://www.paypalobjects.com";
const PAYPAL_RAHMEN = "https://www.paypal.com https://www.sandbox.paypal.com";
const PAYPAL_BILDER = "https://www.paypal.com https://www.paypalobjects.com";
const PAYPAL_VERBINDUNG = "https://www.paypal.com https://www.paypalobjects.com https://www.sandbox.paypal.com";
/* Google-Anmeldung: Google nennt ausdrücklich die ELTERN-Adresse
.../gsi/ statt einzelner Endpunkte -- damit die Anmeldung nicht bricht,
wenn Google intern eine Adresse ändert. */
const GOOGLE_SKRIPT = "https://accounts.google.com/gsi/client";
const GOOGLE_RAHMEN = "https://accounts.google.com/gsi/";
const GOOGLE_VERBINDUNG = "https://accounts.google.com/gsi/";
/* Der Anmelde-Knopf holt sein Aussehen als eigene Stilvorlage. Wichtig:
'unsafe-inline' weiter unten deckt das NICHT ab -- das erlaubt nur
Stile IM Dokument, keine nachgeladene Datei von fremder Adresse. Ohne
diesen Eintrag erscheint der Knopf unfertig/unformatiert. */
const GOOGLE_STIL = "https://accounts.google.com/gsi/style";
/* Gemerkte Prüfsummen je Datei, zusammen mit der Änderungszeit, aus der
sie stammen. Ändert sich die Datei, wird neu gerechnet. */
const merker = new Map();
@@ -151,13 +206,13 @@ export function inhaltsrichtlinie(wurzel) {
wenn ohnehin etwas grundlegend nicht stimmt. */
if (!summen) return next();
const skript = ["'self'", ...summen].join(" ");
const skript = ["'self'", ...summen, PAYPAL_SKRIPT, GOOGLE_SKRIPT].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'",
`style-src 'self' 'unsafe-inline' ${GOOGLE_STIL}`,
/* data: für eingebettete Kleingrafiken, blob: für selbst erzeugte
Vorschauen beim Hochladen.
@@ -169,12 +224,12 @@ export function inhaltsrichtlinie(wurzel) {
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",
`img-src 'self' data: blob: https://postfach.dogfather-universe.com ${PAYPAL_BILDER}`,
"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",
`connect-src 'self' https://postfach.dogfather-universe.com ${PAYPAL_VERBINDUNG} ${GOOGLE_VERBINDUNG}`,
/* Kein Flash, keine Java-Applets, keine eingebetteten Objekte. */
"object-src 'none'",
/* Verhindert, dass ein eingeschleustes <base> alle relativen
@@ -209,8 +264,10 @@ export function inhaltsrichtlinie(wurzel) {
Google-Anmeldung auf abonnieren.html arbeiten ebenfalls mit
Rahmen), muss er hier NAMENTLICH ergänzt werden -- sonst
wiederholt sich exakt dieser stille Ausfall an der Kasse.
pruef-musik.mjs prüft das im echten Browser mit. */
"frame-src https://open.spotify.com",
pruef-musik.mjs und pruef-kasse.mjs prüfen das im echten Browser
mit -- inklusive der Kasse, die vorher genau hier gescheitert
wäre. */
`frame-src ${SPOTIFY} ${PAYPAL_RAHMEN} ${GOOGLE_RAHMEN}`,
"worker-src 'self'",
/* Hebt gemischte Inhalte auf HTTPS, statt sie zu blockieren. */
"upgrade-insecure-requests",