Erscheinungsbild
Content Security Policy
Die Richtlinie entsteht beim Bauen aus zwei Quellen: der Projektkonfiguration und dem erzeugten HTML. Sie wird als Header über die .htaccess ausgeliefert und gilt deshalb nur bei deployment.target: 'mittwald-static' (ADR 0013).
Was drinsteht und warum
text
default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'self';
form-action 'self'; script-src 'self' 'sha256-…' 'sha256-…';
style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';
connect-src 'self'; frame-src 'none'; media-src 'self'; manifest-src 'self'Das ist die Richtlinie eines Projekts ohne Tracking und ohne Captcha. Jede Fremdquelle kommt erst hinzu, wenn die Konfiguration sie einschaltet:
| Eingeschaltet durch | Was dazukommt |
|---|---|
tracking.enabled | www.googletagmanager.com in script-src, connect-src, img-src |
tracking.gtmServerUrl | der Ursprung des serverseitigen Containers |
tracking.ga4 | die Messadressen von GA4 in connect-src und img-src |
tracking.googleAds | die Adressen von Google Ads, auch in frame-src |
forms.captcha: 'turnstile' | challenges.cloudflare.com in script-src und frame-src |
forms.provider: 'joomla' | der Ursprung des Backends in connect-src |
Eine Richtlinie, die Google erlaubt, obwohl das Projekt kein Tracking hat, behauptet eine Verbindung, die es nicht gibt — dieselbe Regel wie bei der Auszeichnung (ADR 0007).
Die Inline-Skripte
Der Starter liefert zwei aus, und beide sind Absicht: das Consent-Skript im Kopf, das laufen muss, bevor irgendetwas anderes lädt (ADR 0008), und ein kleines Skript für die Navigation, das Astro so klein bündelt, dass es im Dokument landet.
Beide werden über ihren SHA-256-Hash abgedeckt. Die Hashes bildet der Build aus dem erzeugten HTML; ändert sich ein Skript, ändert sich der Hash im selben Build. Ein Post-Build-Test bildet sie noch einmal aus dem ausgelieferten HTML und vergleicht sie mit der .htaccess — genau dieser Fehler wäre sonst still: Die Website lädt, und erst beim Umschalten auf enforce bleibt der Consent-Banner weg.
'unsafe-inline' bei script-src ist ausgeschlossen, auch über additionalSources. Damit wäre die Richtlinie für den einzigen Angriff wirkungslos, gegen den sie schützen soll.
JSON-LD braucht keinen Hash
Ein <script type="application/ld+json"> ist ein Datenblock. Der Browser führt ihn nie aus, script-src prüft ihn deshalb nicht. Ihn zu hashen würde die Richtlinie um einen Eintrag je Seite verlängern — bei achtzig Seiten um achtzig — und keinen Angriff verhindern.
Warum Styles inline erlaubt sind
style-src 'unsafe-inline' ist eine bewusste Ausnahme. 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.
Vom Beobachten zum Erzwingen
Vorgabe ist report-only: Der Browser meldet Verstöße und blockiert nichts.
ts
csp: {
mode: 'report-only', // Vorgabe
},Eine erzwungene Richtlinie, die eine Kleinigkeit übersieht, macht die Website unbedienbar. Beim Tag Manager ist das keine theoretische Sorge: Welche Adressen ein Container anspricht, entscheidet der Container. Ein Custom-HTML-Tag mit Inline-Skript ist in GTM zwei Klicks weit weg und in keiner Konfiguration dieses Repositories sichtbar.
Der Weg zu enforce:
- Report-Only ausliefern und die Website benutzen — mit Formular, mit Einwilligung, mit abgelehnter Einwilligung.
- Konsole lesen. Jeder Verstoß nennt die Direktive und die blockierte Adresse:text
Refused to load the script 'https://…' because it violates the following Content Security Policy directive: "script-src 'self' …" - Prüfen, ob die Quelle dahin gehört. Eine Adresse, die niemand erklären kann, ist ein Befund und keine Ergänzung.
- Legitime Quellen eintragen in
csp.additionalSources, unter der genannten Direktive. - Alle Seiten und alle Zustände durchgehen — auch die 404-Seite, auch das abgeschickte Formular, auch den Banner beim zweiten Besuch.
- Umstellen auf
mode: 'enforce'und dasselbe noch einmal durchgehen.
Mit reportUri gehen die Meldungen zusätzlich an einen Sammeldienst. Ohne den Eintrag bleiben sie in der Konsole — für die Entwicklung genügt das.
Quellen ergänzen
ts
csp: {
additionalSources: {
'frame-src': ['https://www.youtube-nocookie.com'],
'img-src': ['https://i.ytimg.com'],
},
},Ergänzbar sind script-src, style-src, img-src, font-src, connect-src, frame-src und media-src. Nicht ergänzbar sind default-src, base-uri, object-src und frame-ancestors: Sie bilden den Rahmen der Richtlinie, und wären sie ergänzbar, könnte ein Projekt sie beiläufig entfernen.
Steht 'none' in einer Direktive und kommt eine echte Quelle dazu, verschwindet 'none' — beides nebeneinander wäre widersprüchlich, und der Browser würde 'none' ohnehin nicht auswerten.
Eine Einbettung ist mehr als eine Direktive
Ein eingebettetes Video braucht in der Regel frame-src für den Player, img-src für das Vorschaubild und connect-src für die Messanfragen. Wer nur die erste einträgt, sieht im Beobachtungsmodus zwei weitere Meldungen — und im erzwungenen Modus ein leeres Rechteck.
Und: Eine Einbettung, die vor der Einwilligung lädt, ist unabhängig von der CSP ein Problem. Verbraucher einer Einwilligung gehören in consentConsumers() (ADR 0008).
Grenzen
- Kein Header auf GitHub Pages. Dort gibt es die Richtlinie nicht; siehe GitHub Pages.
- Kein
'strict-dynamic'. Es lässt Host-Quellen unbeachtet und verlangt für jedes im Markup stehende Skript einen Nonce oder Hash. Astros Bündel stehen als<script src>im Dokument und wären blockiert — die Richtlinie wäre strenger, die Website kaputt. - Keine Nonces. Sie müssten je Antwort neu gebildet werden, und eine statische Datei hat keine Antwortzeit.
- Die Google-Adressen sind ein Ausgangspunkt, keine Zusage. Was ein GTM-Container wirklich anspricht, steht im Container.