Eine Content Security Policy zu deployen ist eines der wirkungsvollsten Sicherheits-Investments — und gleichzeitig eines der operativ herausforderndsten. Theorie (siehe Kap 11 CSP) ist eine Sache; eine bestehende App mit Hunderten Skript-Inklusionen, Drittpartei-Tags und Marketing-Pixeln auf eine restriktive CSP zu bringen ohne Production-Bruch ist eine andere. Dieser Artikel zeigt die schrittweise Rollout-Strategie, Nonce-Patterns und den Umgang mit dem Drittpartei-Problem.
Rollout in vier Phasen
Phase 1 — Report-Only-Modus:
CSP wird gesendet, aber nicht enforced. Violations werden geloggt; nichts ist im UI broken.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://cdn.example.com; report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="https://csp-report.example.com/report"Browser meldet jede Violation an den Reporting-Endpoint, blockiert aber nichts. Du sammelst Daten, was alles in deiner App geladen wird.
Phase 2 — Analyse:
Violations einsammeln (eigenes Backend oder SaaS wie Sentry, Report URI). Pro Verstoß: gehört das hierhin (dann Allowlist erweitern) oder ist es ein Bug (Inline-Script vergessen, Drittpartei-Pixel unautorisiert)?
Phase 3 — Iteration:
CSP-Policy anpassen, Phase 1 wiederholen. Pro Iteration:
- Bekannte False-Positives erlauben.
- Echte Issues fixen (Inline-Scripts in Templates eliminieren, Drittpartei-Skripte verstehen/entfernen).
- Restriktiver werden (
unsafe-inlineraus, dannunsafe-eval, dann externe Domains).
Phase 4 — Enforcement:
Header umstellen von Content-Security-Policy-Report-Only auf Content-Security-Policy. Reporting bleibt — neue Violations sind dann echte Probleme oder neue Anforderungen.
Realistische Timeline: für eine mittelgroße App 1–3 Monate. Für eine große Marketing-Site mit vielen Drittpartei-Tags eher 6–12 Monate.
Nonce-basierte Policy als Default
Warum nicht Hash-basiert: Hashes sind statisch — bei dynamisch generiertem Inline-Skript ungeeignet.
Warum nicht Allowlist: klassische Allowlist mit URLs (script-src https://cdn.example.com) ist anfällig für JSONP-Bypasses und Open-Redirect-Bypasses. Wer eine Lib auf einem in der Allowlist stehenden CDN findet, die JSONP-Endpoints exponiert, kann beliebigen Code laden.
Nonce-basiert:
Content-Security-Policy: default-src 'self';
script-src 'nonce-rAnd0mB4se64' 'strict-dynamic';
object-src 'none';
base-uri 'self';
report-to csp-endpointNonce-Generierung pro Request:
const crypto = require('crypto');
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString('base64');
res.setHeader('Content-Security-Policy', [
"default-src 'self'",
`script-src 'nonce-${res.locals.cspNonce}' 'strict-dynamic'`,
"object-src 'none'",
"base-uri 'self'",
].join('; '));
next();
});
// Im Template (z. B. EJS, Pug):
// <script nonce="<%= cspNonce %>">...</script>Pflicht:
- Nonce muss kryptografisch zufällig sein (mindestens 128 Bit Entropie).
- Pro Request neu generiert — niemals cachen.
- Im HTML-Template als
nonce-Attribut auf jedem<script>-Tag. - HTML-Response als
Cache-Control: private— sonst cached der CDN die Response mit der Nonce für andere User.
strict-dynamic — der Game-Changer
'strict-dynamic' (CSP Level 3) ändert die Semantik: ein erlaubtes Script kann seinerseits weitere Scripts laden, ohne dass diese in der Allowlist stehen müssen.
Praktischer Effekt: Du musst nur deine eigenen Top-Level-Scripts mit Nonce versehen. Diese können dann beliebige Libraries laden (jQuery, React, Google Analytics, etc.) — Browser akzeptiert, weil das Top-Level-Script transitive Trust hat.
<!-- Nur das Top-Level-Script hat Nonce -->
<script nonce="rAnd0mB4se64">
// Dieses Script darf jetzt beliebig weiter Skripte laden
const ga = document.createElement('script');
ga.src = 'https://www.googletagmanager.com/gtag/js?id=GA_ID';
document.head.appendChild(ga);
</script>
<!-- jQuery direkt aus CDN — kein Nonce nötig, weil aus dem nonce-Script geladen -->
<!-- Nur möglich, wenn das ladende Script den korrekten nonce hatte -->Mit strict-dynamic wird:
- URL-Allowlists obsolet — keine
script-src https://cdn1.example.com https://cdn2.example.com ...mehr. - Migration deutlich einfacher — viele Apps können auf einen kleinen Set Nonce-Scripts reduziert werden.
- JSONP-Bypass-Klassen entschärft — auch wenn eine Lib JSONP exponiert, hat sie kein Nonce.
Vorsicht: alle eigenen Top-Level-Scripts müssen Nonce bekommen. Wer einen vergisst, hat broken Functionality.
Reporting-Endpoints
Reporting-API (modern, CSP Level 3):
Reporting-Endpoints: csp-endpoint="https://csp-report.example.com/report"
Content-Security-Policy: ...; report-to csp-endpointLegacy report-uri (CSP Level 2, breit unterstützt):
Content-Security-Policy: ...; report-uri https://csp-report.example.com/reportPragmatisch: beide gleichzeitig setzen — moderne Browser nehmen report-to, ältere report-uri.
Endpoint-Implementation:
// Endpoint nimmt JSON-Reports an
app.post('/csp-report', express.json({ type: ['application/csp-report', 'application/reports+json'] }),
(req, res) => {
const report = req.body['csp-report'] || req.body;
logger.warn('csp.violation', {
blockedUri: report['blocked-uri'],
violatedDirective: report['violated-directive'],
documentUri: report['document-uri'],
originalPolicy: report['original-policy'],
});
res.status(204).end();
}
);SaaS-Alternativen: Sentry, Report URI, Datadog haben CSP-Reporting eingebaut. Spart eigene Infra für Logging und Aggregation.
Achtung: Reporting-Endpoint kann viel Traffic bekommen — manche Bots/Scrapers triggern viele Violations. Rate-Limit auf dem Endpoint, sonst eigenes DoS-Vektor.
Inline-Skripte und -Styles eliminieren
Das größte Migration-Hindernis in bestehenden Apps: Inline-Skripte und -Styles im HTML, die unsafe-inline brauchen.
Klassische Quellen:
<!-- Inline-Script im Body -->
<script>console.log('hi');</script>
<!-- Inline-Event-Handler -->
<button onclick="doStuff()">Klick</button>
<!-- Inline-Style -->
<div style="color: red">...</div>
<!-- javascript:-URL -->
<a href="javascript:void(0)">Link</a>Saubere Patterns:
- Externe Scripts mit Nonce:
<script nonce="..." src="/app.js"></script>. addEventListenerstattonclick-Attribute.- CSS-Klassen statt Inline-Styles.
href="#"plus Event-Handler stattjavascript:.
Migration-Werkzeuge:
'unsafe-hashes'(CSP Level 3) — erlaubt spezifische Inline-Event-Handler per Hash. Migration-Krücke, kein Long-term-Pattern.- CSP Report-Modus identifiziert pro Page, welche Inline-Pattern noch existieren.
- Build-Step, der bei Inline-Scripts Build-Failure auslöst.
Drittpartei-Skripte — das harte Problem
Wenn Marketing, Analytics, Werbe-Tags, Tag-Manager und Embeds (YouTube, Twitter, Maps) ins HTML eingebunden werden, ist die CSP-Migration kompliziert.
Klassen:
-
Eigene Drittpartei-Skripte (Analytics, Crashreporter) — bekannt, dokumentiert,
strict-dynamicplus Nonce auf den Lader-Tag löst es. -
Tag-Manager (GTM, Adobe Launch) — Tag-Manager lädt dynamisch viele weitere Skripte. Mit
strict-dynamicfunktioniert das automatisch — der TM-Script ist mit Nonce versehen, alle ladenden Skripte transitive trusted. -
Werbe-Tags — oft eval-lastig (DoubleClick, header bidding). Brauchen oft
unsafe-eval. Strikte CSP plus Werbe-Tags ist Konflikt. -
Social-Embeds (Twitter-Widget, Facebook-Like-Button) — können oft entfernt und durch Server-side rendered Alternativen ersetzt werden.
-
Marketing-Pixel (Hotjar, Optimizely) — eval-lastig, viele Origins. Pro Tag prüfen, ob notwendig.
Pragmatischer Pfad:
script-srcmit Nonce + strict-dynamic als Default.- Pro problematischen Tag entscheiden: lockern, ersetzen, oder weglassen.
unsafe-evalals Notlösung wenn ein Werbe-Tag es zwingend braucht — bewusste Entscheidung, dokumentiert.
Helpful Tools:
- CSP Evaluator prüft Policy auf häufige Schwächen.
- CSP-Eval-Library für CI-Integration.
Häufige Directives — Quick-Reference
| Directive | Wofür | Empfehlung |
|---|---|---|
default-src | Fallback für unspezifizierte Resource-Typen | 'self' |
script-src | JavaScript | 'nonce-...' 'strict-dynamic' |
style-src | CSS | 'self' 'nonce-...' oder Hash |
img-src | Bilder | 'self' data: https: (sehr restriktiv: nur 'self') |
connect-src | XHR/fetch/WebSocket | 'self' plus erlaubte API-Origins |
font-src | Fonts | 'self' (oder Google Fonts URL) |
frame-src | iframes | 'self' oder explizite URLs |
frame-ancestors | Wer darf diese Seite einbetten | 'none' oder explizite Parent-URLs |
object-src | Plugins (Flash, etc.) | 'none' immer |
base-uri | <base>-Tag-Inhalt | 'self' |
form-action | Formular-Submit-Ziele | 'self' |
upgrade-insecure-requests | http:// zu https:// auto-upgraden | aktivieren bei Mixed-Content-Migration |
Minimal-Pflicht-Set für moderne App:
Content-Security-Policy:
default-src 'self';
script-src 'nonce-...' 'strict-dynamic';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
form-action 'self';
report-to csp-endpointCSP für SPAs
SPAs haben eine besondere Situation: HTML kommt vom CDN (statisch), API von einem anderen Origin.
Pattern:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'sha256-...' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
connect-src 'self' https://api.example.com wss://api.example.com;
img-src 'self' data: https://cdn.example.com;
object-src 'none';
base-uri 'self';Besonderheiten bei SPAs:
- Nonce funktioniert nicht gut — Hashes oder strict-dynamic sind praktischer für CDN-Hosted-HTML (Nonce müsste pro Request neu).
connect-srcmuss alle API-Origins enthalten (HTTP + WebSocket).style-src 'unsafe-inline'ist oft unvermeidbar wegen styled-components, emotion etc. — Trade-off bewusst.- Trusted Types sind die nächste Schicht für DOM-XSS-Schutz, siehe Kap 11 trusted-types.
Besonderheiten
Report-Only und Enforced parallel
Während der Migration: Enforced CSP mit aktuell stabiler Policy + Report-Only CSP mit nächster, strikterer Version. Browser respektiert beide unabhängig. Damit testest du in Production die nächste Iteration, ohne den User zu brechen. Funktioniert mit beiden Headern parallel.
Nonce muss wirklich pro Request neu sein
Wer nonce einmal pro Server-Start generiert und für alle Requests nutzt, hat keinen XSS-Schutz — Angreifer kann die Nonce einmal abgreifen und beliebige Scripts mit der Nonce einschleusen. Cryptographically random pro Request, nicht cacheable — strikt.
CDN-Cache und Nonce-Konflikt
Wenn das HTML mit Nonce vom CDN gecached wird, sehen mehrere User die gleiche Nonce — Vorteil weg. Lösung: HTML-Responses als Cache-Control: private markieren (kein CDN-Cache), oder Nonce-Variante mit ETag/Edge-Worker dynamic einfügen. Cloudflare und Fastly haben Edge-Worker-Patterns dafür.
CSP-Violation-Reports leaken URLs
Im Violation-Report steht document-uri, referrer und teils source-file. Wenn die App private URLs (mit Session-Token im Pfad) hat, leaken die zum Reporting-Endpoint. Sensitive Apps: Report-Endpoint im eigenen Trust-Domain, Token im Pfad nicht verwenden.
Browser-Extensions können Reports triggern
Ad-Blocker, Crypto-Wallets, Translate-Erweiterungen injizieren oft Inline-Scripts oder externe Resources — was als CSP-Violation reported wird. Erwartete Noise im Reporting. Filter pattern für bekannte Extension-IDs (Chrome-Extension-URLs) helfen, das zu reduzieren.
CSP-Bypass über Whitelist-Domains mit JSONP
Klassische Whitelist script-src https://accounts.google.com ist anfällig — Google hostet JSONP-Endpoints, die beliebigen Code returnen können. strict-dynamic löst das strukturell. Bei nicht-strict-dynamic-Setups: pro Whitelist-Eintrag prüfen, ob die Domain JSONP exponiert.
frame-ancestors ersetzt X-Frame-Options
frame-ancestors 'none' in CSP entspricht X-Frame-Options: DENY. frame-ancestors 'self' entspricht SAMEORIGIN. Plus: frame-ancestors kann mehrere Origins. Wer modernen Browsern reicht, kann X-Frame-Options weglassen — alte Browser brauchen es noch als Fallback (2026 selten).
Weiterführende Ressourcen
Externe Quellen
- W3C — Content Security Policy Level 3
- OWASP CSP Cheat Sheet
- Google CSP Evaluator
- web.dev — Strict CSP
- content-security-policy.com — Referenz
- Report URI — CSP-Reporting-Service
- Sentry — CSP Reporting
Verwandte Artikel
- Content Security Policy (Konzept) (Kap 11)
- Trusted Types (Kap 11)
- Secure-Headers-Übersicht
- HSTS und HTTPS-Only
- COOP/COEP/CORP
- XSS-Grundlagen (Kap 11)