Skip to content

0013 — Content Security Policy aus dem Build, Report-Only als Vorgabe ​

Status: angenommen · Milestone: M8

Kontext ​

Der Starter liefert zwei Inline-Skripte aus, und beide sind Absicht. Das Consent-Skript im Kopf muss laufen, bevor irgendetwas anderes lädt (ADR 0008); ein kleines Skript für die Navigation bündelt Astro so klein, dass es im Dokument landet. Dazu kommt die JSON-LD-Ausgabe.

Eine Content Security Policy, die Inline-Skripte erlaubt, indem sie 'unsafe-inline' setzt, schützt gegen nichts — genau das, wovor sie schützen soll, ist damit erlaubt. Zu entscheiden ist also, wie die Inline-Skripte abgedeckt werden, welche Fremdquellen die Richtlinie nennt und ob sie erzwungen wird.

Nonces fallen dabei sofort aus: Sie müssen je Antwort neu gebildet werden, und eine statische Datei hat keine Antwortzeit.

Entscheidung ​

Die Richtlinie entsteht aus zwei Quellen zur Bauzeit. Aus der Projektkonfiguration kommen die Fremdquellen — Tag Manager, GA4-, Ads- und Turnstile-Adressen, das Joomla-Backend, ein serverseitiger Container. Aus dem erzeugten HTML kommen die SHA-256-Hashes der Inline-Skripte. Beides führt buildCsp() in src/lib/deployment/csp.ts zusammen.

Eine Quelle wird nur genannt, wenn das Projekt sie eingeschaltet hat. Ohne tracking.enabled steht keine Google-Adresse in der Richtlinie.

'unsafe-inline' bleibt bei script-src ausgeschlossen — auch über csp.additionalSources. Das Schema lehnt es dort ab.

Bei style-src ist 'unsafe-inline' gesetzt. Bewusst und dokumentiert.

JSON-LD wird nicht gehasht. Ein <script type="application/ld+json"> ist ein Datenblock, den der Browser nie ausführt und den script-src deshalb nicht prüft.

Vorgabe ist report-only. Der Wechsel auf enforce ist eine Entscheidung des Projekts, nach Sichtung der Meldungen.

Begründung ​

Warum Hashes tragbar sind. Die Sorge bei einem Hash-Ansatz ist, dass die Zahl der Hashes mit der Zahl der Seiten wächst und der Header unbrauchbar wird. Für diesen Starter trifft das nicht zu: Der Ladeplan ist auf jeder Seite derselbe, das Navigationsskript ebenfalls. Über alle Seiten und alle drei Ausgaben entstehen zwei verschiedene Hashes. Damit das so bleibt, begrenzt das Performance-Budget die Zahl der verschiedenen Inline-Skripte (ADR 0015) — wächst sie, ist das ein Hinweis, ein Skript in eine Datei zu legen, und keine Einladung, den Header zu verlängern.

Warum kein 'strict-dynamic'. Es wäre der modernere Ansatz, ist hier aber unbrauchbar: 'strict-dynamic' lässt Host-Quellen und 'self' unbeachtet und verlangt für jedes im Markup stehende Skript einen Nonce oder Hash. Astro schreibt seine Bündel als <script type="module" src="/_astro/…"> ins Dokument. Ein Hash für eine externe Datei setzt das Attribut integrity voraus, das Astro nicht erzeugt. Die Bündel wären also blockiert — die Richtlinie wäre strenger, die Website kaputt.

Warum 'unsafe-inline' bei style-src in Kauf genommen wird. Astro schreibt kleine Stylesheets in den Kopf und setzt für die Bildpresets style-Attribute an einzelne Elemente. Attribute deckt ein Hash grundsätzlich nicht ab; dafür bräuchte es zusätzlich 'unsafe-hashes', und übrig blieben Hunderte Hashes für Layoutdetails, die sich mit jedem Bild ändern. Der Unterschied zum Skriptfall ist wesentlich: Aus eingeschleustem CSS entsteht keine Codeausführung. Ein Test hält die Ausnahme fest, damit sie eine Ausnahme bleibt.

Warum die Vorgabe nur meldet. Eine erzwungene Richtlinie, die eine Kleinigkeit übersieht, macht die Website unbedienbar — im schlimmsten Fall bleibt der Consent-Banner weg, und damit läuft eine Website ohne Einwilligungsmöglichkeit. Der Tag Manager verschärft das: Welche Adressen ein Container wirklich anspricht, entscheidet der Container und nicht dieses Repository. Ein eigenes Custom-HTML-Tag mit Inline-Skript ist in GTM zwei Klicks weit weg und in keiner Konfiguration dieses Starters sichtbar. Deshalb sind die Google-Adressen ein belastbarer Ausgangspunkt und keine Zusage; der Weg zu enforce führt über die Meldungen eines echten Projekts.

Warum ein Projekt Quellen ergänzen, aber nicht aufweichen darf. additionalSources ist auf Direktiven beschränkt, die Inhalte laden. default-src, object-src, base-uri und frame-ancestors bilden den Rahmen der Richtlinie; wären sie ergänzbar, könnte ein Projekt sie beiläufig entfernen. Und wer ein Inline-Skript braucht, bekommt seinen Hash — den trägt der Build von selbst ein.

Verworfene Alternativen ​

'unsafe-inline' bei script-src, „vorläufig". Damit ist die Richtlinie für den einzigen Angriff wirkungslos, gegen den sie schützen soll, und der Vorbehalt hätte nach dem dritten Kundenprojekt niemand mehr im Kopf.

Die Inline-Skripte in Dateien auslagern und script-src 'self' setzen. Für das Navigationsskript ginge das. Für das Consent-Skript nicht: Es muss vor allem anderen laufen, und eine externe Datei ist eine zweite Anfrage, deren Zeitpunkt niemand garantiert. Genau diese Reihenfolge ist der Kern von ADR 0008.

Die Richtlinie von Hand in einer Textdatei pflegen. Sie beschreibt nach der dritten Änderung eine Website, die es nicht mehr gibt — und der Hash eines geänderten Consent-Skripts wäre still falsch. Ein Post-Build-Test bildet die Hashes deshalb aus dem ausgelieferten HTML neu und vergleicht sie mit der erzeugten Datei.

Die Richtlinie über ein <meta http-equiv> ausliefern. Sie käme ohne Server aus und würde damit auch auf GitHub Pages wirken. Aber frame-ancestors, report-uri und sandbox sind im Meta-Element unwirksam, und die Richtlinie stünde in jedem Dokument statt an einer Stelle. Für GitHub Pages bleibt es dabei: keine Header, und die Dokumentation sagt das.

Konsequenzen ​

  • Ändert sich das Consent-Skript, ändert sich die Richtlinie. Beide entstehen im selben Build, können also nicht auseinanderlaufen.
  • Ohne reportUri erscheinen Verstöße nur in der Browser-Konsole. Für die Entwicklung genügt das; ein Projekt mit Sammeldienst trägt den Endpunkt in deployment.apache.csp.reportUri ein.
  • Auf GitHub Pages gibt es die Richtlinie nicht — dort werden keine Header ausgeliefert.
  • Ein Kundenprojekt, das einen Kartendienst oder ein Video einbettet, ergänzt seine Quellen in csp.additionalSources. Ohne diesen Eintrag lädt die Einbettung nicht, sobald enforce gilt.
  • Die Prüfreihenfolge auf dem Weg zu enforce steht unter Security → CSP.