Leitfaden zur Konfiguration von Content Security Policy
Wie du Content-Security-Policy-Header umsetzt, die wirklich schützen: Direktiven-Syntax, nonce-basierte Inline-Skripte, Reporting und sanfter Rollout.

Content Security Policy (CSP) ist die stärkste Verteidigung gegen Cross-Site Scripting (XSS). Sie teilt dem Browser mit, welche Inhaltsquellen erlaubt sind – Skripte, Styles, Bilder, Schriftarten und Verbindungen. Wenn ein Angreifer ein Script-Tag in deine Seite einschleust, blockiert der Browser es, weil die Skriptquelle nicht auf deiner CSP-Allowlist steht.
Die meisten Teams überspringen CSP, weil sie es für zu komplex halten oder befürchten, dass es ihre Website kaputt macht. In Wahrheit lässt sich eine sauber konfigurierte CSP schrittweise einführen – beginne im Report-Only-Modus, behebe die Verstöße und aktiviere dann die Durchsetzung. Dieser Leitfaden führt durch den gesamten Prozess von null bis zur vollständigen Durchsetzung.
Grundlagen der CSP-Direktiven
Eine CSP ist ein Satz von Direktiven, von denen jede einen Ressourcentyp steuert. Jede Direktive legt fest, welche Quellen erlaubt sind.
// The most common CSP directives
const cspDirectives = {
"default-src":
"Fallback for all resource types not explicitly listed",
"script-src":
"JavaScript sources (most critical for XSS prevention)",
"style-src":
"CSS sources",
"img-src":
"Image sources",
"font-src":
"Font sources",
"connect-src":
"URLs for fetch(), XHR, WebSocket, EventSource",
"frame-src":
"Sources for <iframe> and <frame>",
"media-src":
"Audio and video sources",
"object-src":
"Plugin sources (<object>, <embed>, <applet>)",
"base-uri":
"URLs allowed in <base> element",
"form-action":
"URLs that forms can submit to",
"frame-ancestors":
"Who can embed this page (replaces X-Frame-Options)",
"report-uri":
"Where to send violation reports (deprecated, use report-to)",
"report-to":
"Reporting endpoint group for violations",
};# ❌ Overly permissive CSP that provides little protection
Content-Security-Policy: default-src * 'unsafe-inline' 'unsafe-eval';
# Allows everything from everywhere — XSS protection is effectively disabled
# ✅ Strict CSP that actually protects against XSS
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-abc123';
style-src 'self' 'nonce-abc123';
img-src 'self' data: https://images.example.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://api.example.com wss://ws.example.com;
frame-ancestors 'none';
object-src 'none';
base-uri 'self';
form-action 'self';
report-to csp-violations;Nonce-basierte CSP für Inline-Skripte
Das Schlüsselwort unsafe-inline macht CSP für Skripte vollständig wirkungslos – jedes eingeschleuste Inline-Skript wird ausgeführt. Nonces lösen dieses Problem: Jeder Seitenaufruf erzeugt ein eindeutiges Zufalls-Token. Nur Script-Tags mit dem passenden Nonce werden ausgeführt.
import crypto from "crypto";
import { Request, Response, NextFunction } from "express";
function cspMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
// Generate a unique nonce per request
const nonce = crypto.randomBytes(16).toString("base64");
// Make nonce available to templates
res.locals.cspNonce = nonce;
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: https://images.example.com`,
`font-src 'self' https://fonts.gstatic.com`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`object-src 'none'`,
`base-uri 'self'`,
`form-action 'self'`,
].join("; ");
res.setHeader("Content-Security-Policy", csp);
next();
}<!-- In your HTML template -->
<!DOCTYPE html>
<html>
<head>
<!-- ❌ Blocked: inline script without nonce -->
<script>console.log("This is blocked by CSP")</script>
<!-- ✅ Allowed: inline script with matching nonce -->
<script nonce="<%= cspNonce %>">
window.__CONFIG__ = { apiUrl: "/api" };
</script>
<!-- ✅ Allowed: external script from 'self' -->
<script src="/js/app.js"></script>
<!-- ❌ Blocked: external script from unknown origin -->
<script src="https://evil.com/steal-data.js"></script>
<!-- ✅ Allowed: style with nonce -->
<style nonce="<%= cspNonce %>">
body { font-family: sans-serif; }
</style>
</head>
<body>
<!-- If an attacker injects via XSS: -->
<!-- <script>document.cookie</script> -->
<!-- Browser blocks it: no nonce, no execution -->
</body>
</html>Schrittweiser Rollout mit dem Report-Only-Modus
Wenn du die CSP-Durchsetzung auf einer komplexen Website sofort aktivierst, geht etwas kaputt. Der Report-Only-Modus sendet Verstoßberichte, ohne Inhalte zu blockieren, sodass du Probleme zuerst identifizieren und beheben kannst.
interface CSPViolationReport {
"csp-report": {
"document-uri": string;
"violated-directive": string;
"blocked-uri": string;
"source-file": string;
"line-number": number;
"column-number": number;
"original-policy": string;
};
}
// Step 1: Deploy in report-only mode
function reportOnlyCSP(nonce: string): string {
return [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}'`,
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data:`,
`connect-src 'self'`,
`report-uri /api/csp-report`,
].join("; ");
}
// Set report-only header (does NOT block anything)
// res.setHeader("Content-Security-Policy-Report-Only", reportOnlyCSP(nonce));
// Step 2: Collect and analyze violation reports
import express from "express";
const app = express();
app.post(
"/api/csp-report",
express.json({ type: "application/csp-report" }),
(req, res) => {
const report = req.body as CSPViolationReport;
const violation = report["csp-report"];
console.log("CSP violation:", {
page: violation["document-uri"],
directive: violation["violated-directive"],
blocked: violation["blocked-uri"],
source: violation["source-file"],
line: violation["line-number"],
});
// Store violations for analysis
// Group by blocked-uri to identify patterns
res.status(204).end();
}
);// Step 3: Fix violations iteratively
// Common violations and their fixes:
const violationFixes = {
"Inline script blocked": {
violation: "script-src: 'inline'",
fix: "Add nonce to the script tag, or move to external file",
},
"Inline style blocked": {
violation: "style-src: 'inline'",
fix: "Add nonce to style tag, or use external stylesheet. " +
"For style attributes, use 'unsafe-hashes' with specific hashes",
},
"Third-party script blocked": {
violation: "script-src: https://cdn.analytics.com",
fix: "Add the domain to script-src directive",
},
"eval() blocked": {
violation: "script-src: 'eval'",
fix: "Refactor code to avoid eval(). If a library requires it, " +
"add 'unsafe-eval' only for that specific context (last resort)",
},
"data: URI blocked": {
violation: "img-src: data:",
fix: "Add 'data:' to img-src if you use base64 images",
},
};
// Step 4: When violations drop to near-zero, switch to enforcement
// res.setHeader("Content-Security-Policy", enforceCSP(nonce));CSP für Single-Page-Applications
SPAs stellen spezifische CSP-Herausforderungen: dynamisches Nachladen von Skripten, CSS-in-JS-Bibliotheken und API-Aufrufe an mehrere Backends.
// Next.js CSP configuration
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";
export function middleware(request: NextRequest) {
const nonce = Buffer.from(crypto.randomUUID()).toString("base64");
const csp = [
`default-src 'self'`,
`script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,
// 'strict-dynamic' trusts scripts loaded by already-trusted scripts
// If app.js (trusted via nonce) loads chunk-abc.js, it's allowed
`style-src 'self' 'nonce-${nonce}'`,
`img-src 'self' data: blob: https://images.example.com`,
`font-src 'self'`,
`connect-src 'self' https://api.example.com`,
`frame-ancestors 'none'`,
`object-src 'none'`,
`base-uri 'self'`,
].join("; ");
const response = NextResponse.next();
response.headers.set("Content-Security-Policy", csp);
// Pass nonce to the page via request header
response.headers.set("x-nonce", nonce);
return response;
}// ❌ CSS-in-JS libraries often use inline styles
// styled-components, emotion, etc. inject <style> tags at runtime
// Without nonce support, they break under strict CSP
// ✅ Configure CSS-in-JS to use nonces
// styled-components: <StyleSheetManager nonce={nonce}>
// emotion: <CacheProvider value={createCache({ nonce })}>
// If the library doesn't support nonces, consider:
// 1. Using CSS modules instead (no runtime injection)
// 2. Using 'unsafe-inline' for styles only (still protects scripts)
// 3. Extracting styles at build time (next/styled-components plugin)
const cspForCSSInJS = {
withNonce: `style-src 'self' 'nonce-${nonce}'`,
fallback: `style-src 'self' 'unsafe-inline'`,
// 'unsafe-inline' for styles is acceptable when script-src is strict
// XSS payload injection is via scripts, not style attributes
};Testen deiner CSP
# Verify CSP headers are set correctly
curl -s -D - https://example.com | grep -i content-security-policy
# Use Google's CSP Evaluator to check for weaknesses
# https://csp-evaluator.withgoogle.com/
# Common mistakes that weaken CSP:
# 1. Using 'unsafe-inline' for script-src (defeats XSS protection)
# 2. Using * in any directive (too permissive)
# 3. Missing object-src 'none' (Flash/plugin-based attacks)
# 4. Missing base-uri 'self' (base tag injection)
# 5. Allowing 'unsafe-eval' without strict necessityDie wichtigsten Erkenntnisse
- CSP ist die stärkste Verteidigung gegen XSS – sie teilt dem Browser mit, welche Inhaltsquellen erlaubt sind; selbst wenn ein Angreifer HTML einschleust, werden nicht autorisierte Skripte nicht ausgeführt
- Verwende Nonces statt
unsafe-inline– erzeuge pro Request ein eindeutiges Zufalls-Nonce und füge es den Script- und Style-Tags hinzu; so bleibt dein Inline-Code erlaubt, während eingeschleuster Code blockiert wird - Beginne mit dem Report-Only-Modus –
Content-Security-Policy-Report-Onlysammelt Verstöße, ohne etwas kaputt zu machen; behebe die Probleme, bevor du zur Durchsetzung wechselst strict-dynamicvereinfacht die CSP für SPAs – es erlaubt Skripte, die von bereits vertrauenswürdigen Skripten geladen werden, sodass du nur deinen Entry Point mit einem Nonce versehen musst, nicht jeden dynamisch geladenen Chunkobject-src: noneundbase-uri: selfsind immer erforderlich – sie schließen plugin-basierte Angriffsvektoren und Base-Tag-Injektion, die in CSP-Konfigurationen oft übersehen werden


