Cross-Site-Scripting (XSS) steht seit Jahren auf der OWASP-Top-10-Liste der gefährlichsten Webanwendungsangriffe. Bei einem XSS-Angriff injiziert ein Angreifer schadhaften JavaScript-Code in deine Webseite, der im Browser des Opfers ausgeführt wird, um Sitzungs-Tokens zu stehlen oder Aktionen im Namen des Nutzers durchzuführen. Der Content Security Policy-Header (CSP) ist die wirksamste Browser-seitige Gegenwehr: Er teilt dem Browser exakt mit, welche Ressourcen geladen werden dürfen, und blockiert alles andere. Dieses Tutorial zeigt, wie du CSP in 12 Schritten in einer Node.js/Express-Anwendung implementierst, von der ersten Direktive bis zur produktionsreifen Konfiguration mit Nonces, Hashes, strict-dynamic und Violation Reporting.
Was ist Content Security Policy und warum brauchst du sie?
Der Content-Security-Policy-Header ist eine HTTP-Response-Header-Richtlinie, die dem Browser mitteilt, welche Quellen er für Skripte, Stylesheets, Bilder, Fonts und andere Ressourcen als vertrauenswürdig akzeptieren soll. Alles, was nicht explizit erlaubt ist, wird blockiert und ein Violation-Report kann an einen konfigurierten Endpunkt gesendet werden.
Ohne CSP kann ein Angreifer, der es schafft, schadhaften HTML-Code in deine Seite einzuschleusen (z. B. durch eine unzureichend escapte Nutzereingabe), beliebigen JavaScript-Code ausführen. Mit einer sorgfältig konfigurierten CSP ist dieser Angriff in der Regel nicht mehr möglich, weil der Browser den injizierten Code verweigert.
CSP Level 2 ist der produktive Baseline, den alle modernen Browser vollständig unterstützen. CSP Level 3 (aktuelle W3C-Spezifikation) bringt Erweiterungen wie 'strict-dynamic' und require-trusted-types-for, die in Chromium-Browsern bereits implementiert sind. Für eine DACH-Nutzerbasis, bei der mehr als 65 % Chrome oder Edge nutzen, sind Level-3-Features 2026 produktionsreif.
Wichtig: CSP ist kein Ersatz für korrektes Input-Escaping oder Parameterized Queries. Es ist eine zusätzliche Verteidigungsschicht, die Angriffe blockiert, die trotz anderer Maßnahmen durchkommen.
Voraussetzungen
Für dieses Tutorial benötigst du folgende Softwareversionen:
| Software | Mindestversion | Empfohlene Version | Zweck |
|---|---|---|---|
| Node.js | 18.x LTS | 20.20.2 LTS | JavaScript-Laufzeitumgebung |
| npm | 9.x | 10.x | Paketverwaltung |
| Express | 4.x | 5.2.1 | Web-Framework |
| helmet | 7.x | 8.2.0 | HTTP-Security-Header-Middleware |
| curl oder Browser DevTools | beliebig | aktuelle Version | CSP-Header testen |
Außerdem brauchst du Grundkenntnisse in Node.js und Express sowie ein Terminal (Linux/macOS) oder die PowerShell/Git Bash unter Windows. Das fertige Projekt findest du am Ende dieses Tutorials als vollständig lauffähigen Code.
Schritt 1: Projektstruktur anlegen
Lege ein frisches Verzeichnis an und initialisiere ein npm-Projekt:
mkdir csp-demo && cd csp-demo
npm init -y
npm install express@5 helmet@8
Die Verzeichnisstruktur nach diesem Schritt:
csp-demo/
├── node_modules/
├── package.json
└── server.js (noch zu erstellen)
Lege die Hauptdatei server.js an:
const express = require('express');
const app = express();
const PORT = 3000;
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html lang="de">
<head><title>CSP Demo</title></head>
<body>
<h1>CSP Demo</h1>
<script>console.log('inline script');</script>
</body>
</html>
`);
});
app.listen(PORT, () => console.log(`Server läuft auf Port ${PORT}`));
Starte den Server mit node server.js und rufe http://localhost:3000 auf. Im Browser-Netzwerktab siehst du noch keinen CSP-Header. Das ändern wir in den nächsten Schritten.
Schritt 2: Ersten CSP-Header manuell setzen
Bevor wir Helmet.js einsetzen, ist es wichtig zu verstehen, wie ein CSP-Header manuell gesetzt wird. Das gibt dir die volle Kontrolle und hilft beim Debuggen späterer Probleme.
const express = require('express');
const app = express();
const PORT = 3000;
// Manuelle CSP-Middleware
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
"script-src 'self'",
"style-src 'self'",
"img-src 'self' data:",
"connect-src 'self'",
"font-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"frame-ancestors 'none'",
"form-action 'self'"
].join('; ')
);
next();
});
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html lang="de">
<head><title>CSP Demo</title></head>
<body>
<h1>CSP aktiv</h1>
<!-- Dieser inline-Script wird von CSP blockiert -->
<script>console.log('dieser Code wird blockiert');</script>
</body>
</html>
`);
});
app.listen(PORT, () => console.log(`Server läuft auf Port ${PORT}`));
Nach dem Neustart des Servers öffne die Browser-Konsole. Du siehst jetzt eine CSP-Fehlermeldung:
Refused to execute inline script because it violates the following Content Security Policy directive:
"script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.
Das ist genau das gewünschte Verhalten. Der Browser verweigert den inline-Script, weil unsere Policy nur Skripte von 'self' (der gleichen Origin) erlaubt. Eine Lösung ist, externe Skript-Dateien statt inline-Code zu verwenden oder Nonces/Hashes einzusetzen, was wir in Schritten 7 und 8 zeigen.
Schritt 3: Helmet.js CSP-Middleware konfigurieren
Helmet.js ist eine Express-Middleware-Sammlung, die zahlreiche HTTP-Security-Header auf einmal setzt. Die aktuelle Version 8.2.0 verwendet für CSP die Methode helmet.contentSecurityPolicy(). Die offizielle Helmet-Dokumentation empfiehlt, Helmet früh in der Middleware-Kette zu platzieren:
const express = require('express');
const helmet = require('helmet');
const app = express();
const PORT = 3000;
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
baseUri: ["'self'"],
frameAncestors: ["'none'"],
formAction: ["'self'"]
}
})
);
app.get('/', (req, res) => {
res.send('<h1>Gesichert mit Helmet CSP</h1>');
});
app.listen(PORT, () => console.log(`Port ${PORT}`));
Hinweis: In diesem Beispiel ist 'unsafe-inline' für styleSrc gesetzt. Das ist für viele Projekte ein pragmatischer Einstieg, da inline-Styles sehr häufig in Bibliotheken vorkommen. Für maximale Sicherheit solltest du auch hier auf Nonces oder Hashes umstellen. Für scriptSrc ist 'unsafe-inline' hingegen ein kritisches Sicherheitsrisiko und sollte nie in der Produktion stehen.
Überprüfe den gesetzten Header mit curl:
curl -I http://localhost:3000
Die Ausgabe sollte einen vollständigen CSP-Header enthalten:
content-security-policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; media-src 'self'; frame-src 'none'; base-uri 'self'; frame-ancestors 'none'; form-action 'self'
Schritt 4: CSP-Direktiven im Überblick
CSP kennt über 20 Direktiven. Die folgende Tabelle zeigt die wichtigsten, ihren Zweck und den empfohlenen Wert für eine restriktive Produktionskonfiguration. Die vollständige Referenz findest du auf MDN Web Docs und auf content-security-policy.com:
| Direktive | Kontrolliert | Empfohlener Produktionswert | Risiko bei Fehler |
|---|---|---|---|
default-src | Fallback für alle Ressourcentypen | 'self' | Alle nicht explizit definierten Typen unkontrolliert |
script-src | JavaScript-Quellen, inline-Scripts, eval() | 'self' 'nonce-XYZ' | XSS über injizierte Scripts möglich |
style-src | CSS-Quellen und inline-Styles | 'self' 'nonce-XYZ' | CSS-Injection, Datenleck über Attribute |
img-src | Bildquellen inkl. data: URIs | 'self' data: https: | Tracking-Pixel von Drittanbietern |
connect-src | fetch(), XHR, WebSocket, EventSource | 'self' | Datenleck an externe Server |
font-src | Web-Fonts | 'self' | Font-Fingerprinting |
object-src | Flash, Plugins, <object> | 'none' | Plugin-basierte Exploits |
frame-ancestors | Wer darf die Seite einbetten? | 'none' oder 'self' | Clickjacking-Angriffe |
base-uri | Erlaubte <base>-Tag-Ziele | 'self' | Base-Tag-Injection rewrite relative URLs |
form-action | Wohin Formulare submitten dürfen | 'self' | Formular-Hijacking an externe URL |
upgrade-insecure-requests | HTTP zu HTTPS upgraden | Immer setzen | Mixed-Content-Warnungen |
report-uri / report-to | Violation-Reporting-Endpunkt | Eigener Endpunkt | Keine Sichtbarkeit bei Verstößen |
Eine wichtige Regel: object-src 'none' immer setzen. Flash und veraltete Browser-Plugins sind bekannte Angriffsflächen, die du vollständig ausschließen solltest. Ebenso wichtig ist base-uri 'self': Ohne diese Direktive kann ein Angreifer ein <base href="https://evil.com">-Tag injizieren und damit alle relativen URLs auf seine Domain umleiten.
Schritt 5 bis 6: Nonce-basierte CSP implementieren
Inline-Scripts und Inline-Styles sind im Webentwicklungsalltag unvermeidlich. Templates-Engines rendern oft dynamische Inhalte als <script>-Blöcke direkt in der HTML-Antwort. Das naheliegendste Lösungskonzept, 'unsafe-inline' zu erlauben, deaktiviert den XSS-Schutz komplett und ist für script-src inakzeptabel.
Die saubere Lösung ist ein Nonce: ein kryptografisch zufälliger Wert, der pro HTTP-Anfrage neu generiert wird und sowohl im CSP-Header als auch im nonce-Attribut des <script>-Tags erscheint. Der Browser erlaubt nur Scripts, deren Nonce mit dem Header übereinstimmt. Ein Angreifer kann den Nonce nicht kennen, da er bei jeder Anfrage neu erstellt wird.
Die Implementierung in Express nutzt das crypto-Modul aus der Node.js-Standardbibliothek:
const express = require('express');
const helmet = require('helmet');
const crypto = require('crypto');
const app = express();
const PORT = 3000;
// Nonce-Middleware: pro Request neu erzeugen
app.use((req, res, next) => {
// 16 Bytes = 128 Bit Entropie, ausreichend für einen Nonce
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
// Helmet mit dynamischem Nonce
app.use((req, res, next) => {
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", `'nonce-${res.locals.nonce}'`],
styleSrc: ["'self'", `'nonce-${res.locals.nonce}'`],
imgSrc: ["'self'", "data:"],
connectSrc: ["'self'"],
objectSrc: ["'none'"],
baseUri: ["'self'"],
frameAncestors: ["'none'"],
formAction: ["'self'"],
upgradeInsecureRequests: []
}
})(req, res, next);
});
app.get('/', (req, res) => {
const nonce = res.locals.nonce;
res.send(`
<!DOCTYPE html>
<html lang="de">
<head>
<title>CSP Nonce Demo</title>
<!-- Nonce im style-Tag erlaubt inline-CSS -->
<style nonce="${nonce}">
body { font-family: sans-serif; padding: 2rem; }
h1 { color: #2563eb; }
</style>
</head>
<body>
<h1>Nonce-gesicherter Inline-Code</h1>
<!-- Nonce im script-Tag erlaubt inline-JavaScript -->
<script nonce="${nonce}">
console.log('Dieser inline-Script ist durch den Nonce erlaubt.');
document.querySelector('h1').textContent = 'Nonce funktioniert!';
</script>
</body>
</html>
`);
});
app.listen(PORT, () => console.log(`Nonce-Demo läuft auf Port ${PORT}`));
Lade die Seite zweimal neu und inspiziere den CSP-Header: Der Nonce-Wert ändert sich bei jeder Anfrage. Öffne die Browser-Konsole und versuche, einen Script ohne Nonce auszuführen. Der Browser verweigert ihn. Das ist der Nonce-basierte Schutz in Aktion.
Kritischer Hinweis: Der Nonce muss wirklich zufällig und per Request frisch sein. Verwende niemals einen statischen Nonce. Ein Angreifer, der den Nonce einmalig ausspäht (z. B. aus einem gecachten Response), kann ihn wiederverwenden. crypto.randomBytes(16) erzeugt ausreichende Entropie (128 Bit), da die Wahrscheinlichkeit einer Kollision in der Praxis null ist.
Schritt 7: Hash-basierte CSP für statische Inline-Scripts
Wenn der Inhalt eines <script>-Blocks sich nie ändert, ist ein Hash eine Alternative zum Nonce. Der Browser berechnet den SHA-256-Hash des Script-Inhalts und vergleicht ihn mit dem im CSP-Header angegebenen Wert. Stimmen sie überein, wird das Script ausgeführt.
So erzeugst du den SHA-256-Hash eines inline-Scripts in Node.js:
const crypto = require('crypto');
// Exakter Script-Inhalt (Whitespace inklusive)
const scriptContent = `console.log('Hallo Welt');`;
const hash = crypto
.createHash('sha256')
.update(scriptContent)
.digest('base64');
console.log(`'sha256-${hash}'`);
// Ausgabe: 'sha256-abc123...'
Den erzeugten Hash trägst du in den scriptSrc-Direktive ein:
const SCRIPT_HASH = `'sha256-${hash}'`;
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", SCRIPT_HASH],
objectSrc: ["'none'"],
baseUri: ["'self'"]
}
})
);
app.get('/', (req, res) => {
res.send(`
<html>
<body>
<!-- Nur dieser exakte Script-Inhalt ist erlaubt -->
<script>console.log('Hallo Welt');</script>
</body>
</html>
`);
});
Der Vorteil von Hashes: Kein dynamisch generierter Wert nötig, der Header kann gecacht werden. Der Nachteil: Sobald du auch nur ein Leerzeichen im Script-Inhalt änderst, ändert sich der Hash und du musst den Header aktualisieren. Hashes eignen sich daher für wirklich statische Scripts (z. B. ein Analytics-Snippet, das sich selten ändert). CSP unterstützt auch SHA-384 und SHA-512 für noch stärkere Hashes.
Schritt 8: Report-Only Modus und Violation Reporting einrichten
Bevor du eine strenge CSP in der Produktion aktivierst, empfiehlt das OWASP CSP Cheat Sheet ausdrücklich, zunächst den Report-Only Modus zu nutzen. Der Header Content-Security-Policy-Report-Only verhält sich wie ein normaler CSP-Header, blockiert aber keine Ressourcen, sondern sendet nur Verletzungsberichte. So siehst du, was eine künftige Policy brechen würde, ohne die Produktionsanwendung zu stören.
const express = require('express');
const app = express();
const PORT = 3000;
const violations = [];
// Report-Only Middleware
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy-Report-Only',
[
"default-src 'self'",
"script-src 'self'",
"style-src 'self'",
"object-src 'none'",
"base-uri 'self'",
"report-uri /csp-report"
].join('; ')
);
next();
});
// CSP Violation Report Endpunkt
app.use(express.json({ type: 'application/csp-report' }));
app.post('/csp-report', (req, res) => {
const report = req.body['csp-report'] || req.body;
console.log('CSP-Verstoß:', JSON.stringify(report, null, 2));
violations.push({
time: new Date().toISOString(),
...report
});
res.status(204).end();
});
// Verstoß-Log einsehen
app.get('/violations', (req, res) => {
res.json(violations);
});
app.get('/', (req, res) => {
res.send(`
<html>
<body>
<!-- Dieser Script würde im enforce-Modus blockiert (kein Nonce/Hash) -->
<script>console.log('Verstoß wird geloggt aber nicht blockiert');</script>
<!-- Bild von externer Domain, würde in production blockiert -->
<img src="https://via.placeholder.com/100" alt="test">
</body>
</html>
`);
});
app.listen(PORT, () => console.log(`Report-Only auf Port ${PORT}`));
Ein typischer Violation-Report vom Browser sieht so aus:
{
"csp-report": {
"document-uri": "http://localhost:3000/",
"referrer": "",
"violated-directive": "script-src 'self'",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'self'; ...",
"blocked-uri": "inline",
"status-code": 200,
"script-sample": "console.log('Verstoß wird geloggt..."
}
}
Die Empfehlung: Setze Report-Only für mindestens 2 Wochen in der Produktion. Analysiere die eingehenden Reports und passe die Policy an, bis keine legitimen Ressourcen mehr blockiert werden. Erst dann wechselst du auf den Enforce-Header. Die modernere Alternative zu report-uri ist report-to in Kombination mit der Reporting-API, die einen strukturierteren Reporting-Mechanismus bietet und in aktuellen Chromium-Browsern unterstützt wird.
Schritt 9 und 10: strict-dynamic und Trusted Types API
strict-dynamic ist ein Schlüsselwort aus CSP Level 3, das die Verwaltung von Nonce-basierten Policies erheblich vereinfacht. Wenn ein Script durch einen Nonce oder Hash als vertrauenswürdig markiert ist und dieses Script seinerseits weitere Scripts dynamisch lädt (document.createElement('script')), gelten diese Kind-Scripts ebenfalls als vertrauenswürdig, ohne dass ihre Quell-URLs explizit in der Policy stehen müssen.
Das OWASP CSP Cheat Sheet empfiehlt als moderne Produktions-Policy:
app.use((req, res, next) => {
const nonce = crypto.randomBytes(16).toString('base64');
res.locals.nonce = nonce;
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
// strict-dynamic: nonce-approved Scripts dürfen weitere Scripts laden
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
"style-src 'self'",
"object-src 'none'",
// base-uri 'none' ist restriktiver als 'self'
"base-uri 'none'",
"frame-ancestors 'none'",
"form-action 'self'",
"upgrade-insecure-requests",
"report-uri /csp-report"
].join('; ')
);
next();
});
Mit strict-dynamic musst du nicht mehr alle CDN-URLs deiner JavaScript-Bibliotheken in der Policy auflisten, was die Policy wartbarer macht und weniger anfällig für JSONP-Bypässe ist.
Trusted Types ist eine ergänzende Browser-API, die DOM-XSS-Angriffe über gefährliche Sinks wie innerHTML, outerHTML oder document.write() verhindert. Anstatt rohe Strings an diese APIs zu übergeben, erfordert Trusted Types das Erstellen einer Policy, die Strings in sichere Objekte transformiert:
// CSP-Header mit Trusted Types
Content-Security-Policy:
require-trusted-types-for 'script';
trusted-types appPolicy;
// Client-seitiger JavaScript-Code
const policy = trustedTypes.createPolicy('appPolicy', {
createHTML(input) {
// DOMPurify als Sanitizer einsetzen
return DOMPurify.sanitize(input);
}
});
// Sicherer Einsatz von innerHTML
document.querySelector('#output').innerHTML =
policy.createHTML(userInput);
Trusted Types ist in Chromium-Browsern (Chrome, Edge) verfügbar und wird schrittweise in anderen Browsern eingeführt. Für Produktionsanwendungen empfiehlt sich Report-Only (Content-Security-Policy-Report-Only: require-trusted-types-for 'script') zum Testen, bevor auf Enforce umgestellt wird.
Schritt 11: CSP für Single Page Applications (React, Vue, Angular)
SPAs stellen besondere Herausforderungen für CSP dar. React und Vue rendern DOM-Elemente dynamisch, und viele Build-Tools injizieren inline-Scripts in die index.html. Folgende Strategie funktioniert für die meisten SPA-Setups mit einem Node.js/Express-Backend:
const express = require('express');
const helmet = require('helmet');
const crypto = require('crypto');
const path = require('path');
const app = express();
// Nonce für jede Anfrage generieren
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
// CSP für SPA: strict-dynamic erlaubt Bundle-Loader
app.use((req, res, next) => {
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
(req, res) => `'nonce-${res.locals.nonce}'`,
"'strict-dynamic'"
],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "blob:"],
connectSrc: [
"'self'",
"https://api.deineapp.de",
"wss://ws.deineapp.de"
],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
upgradeInsecureRequests: []
},
reportOnly: false
})(req, res, next);
});
// SPA: Index-HTML dynamisch rendern um Nonce einzusetzen
app.get('/', (req, res) => {
const nonce = res.locals.nonce;
// In Produktion: Template-Engine oder fs.readFile verwenden
res.send(`
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>React App</title>
<link rel="stylesheet" href="/static/css/main.css">
</head>
<body>
<div id="root"></div>
<!-- Nonce ermöglicht den Bundle-Loader und React hydration -->
<script nonce="${nonce}" src="/static/js/main.js"></script>
</body>
</html>
`);
});
// Statische Assets servieren
app.use('/static', express.static(path.join(__dirname, 'build/static')));
app.listen(3000);
Wichtige Punkte für SPAs: 'unsafe-inline' für styleSrc ist bei vielen UI-Bibliotheken (Material UI, Ant Design) vorerst nötig, da sie Styles direkt in den DOM schreiben. Für connectSrc musst du explizit alle API-Endpunkte und WebSocket-URLs eintragen. blob: in imgSrc ist oft nötig, wenn Nutzer Bilder hochladen und diese über URL.createObjectURL() angezeigt werden.
Schritt 12: CSP testen, debuggen und finalisieren
Bevor du die CSP in der Produktion schaltest, solltest du sie gründlich testen. Drei Werkzeuge sind hier unverzichtbar:
1. Browser DevTools: In Chrome und Firefox gibt die Konsole detaillierte CSP-Fehlermeldungen aus, welche Direktive verletzt wurde und welche Ressource blockiert wird. Öffne DevTools mit F12, wechsle zum Reiter “Konsole” und lade die Seite neu. Alle CSP-Verstöße erscheinen als rote Fehlermeldungen.
2. CSP Evaluator: Das Tool CSP Evaluator von Google analysiert deine Policy auf bekannte Schwachstellen, Bypass-Möglichkeiten und Konfigurationsfehler. Füge deinen CSP-Header-Wert ein und erhalte eine detaillierte Bewertung.
3. Automatisierter Test in Node.js:
const http = require('http');
function testCSP(port = 3000) {
return new Promise((resolve, reject) => {
http.get(`http://localhost:${port}/`, (res) => {
const csp = res.headers['content-security-policy'];
if (!csp) {
reject(new Error('Kein CSP-Header gefunden!'));
return;
}
const checks = {
defaultSrc: csp.includes("default-src 'self'"),
noUnsafeInlineScript: !csp.match(/script-src[^;]*'unsafe-inline'/),
noUnsafeEval: !csp.includes("'unsafe-eval'"),
objectSrcNone: csp.includes("object-src 'none'"),
baseUriRestricted: csp.includes("base-uri"),
frameAncestors: csp.includes("frame-ancestors"),
hasNonce: csp.includes("nonce-") || csp.includes("sha256-")
};
console.log('CSP-Prüfergebnis:');
Object.entries(checks).forEach(([key, value]) => {
console.log(` ${value ? '✓' : '✗'} ${key}`);
});
const passed = Object.values(checks).filter(Boolean).length;
console.log(`\n${passed}/${Object.keys(checks).length} Checks bestanden`);
resolve(checks);
}).on('error', reject);
});
}
testCSP().catch(console.error);
Das Skript prüft die wichtigsten Sicherheitskriterien und gibt eine übersichtliche Zusammenfassung aus. Führe es als Teil deiner CI/CD-Pipeline aus, um sicherzustellen, dass die CSP-Konfiguration nach jedem Deployment korrekt ist.
Für die finale Produktionskonfiguration empfiehlt die W3C CSP Level 3 Spezifikation folgende Reihenfolge beim Rollout: (1) Report-Only mit einer strikten Policy für 2 Wochen, (2) Analyse der Violation Reports, (3) Policy anpassen bis alle legitimen Ressourcen erlaubt sind, (4) Umschalten auf Enforce-Modus, (5) Weiteres Monitoring über den Reporting-Endpunkt.
Häufige Fehler bei der CSP-Implementierung
Diese fünf Fehler führen bei CSP-Implementierungen am häufigsten zu Sicherheitslücken oder broken Deployments:
Fehler 1: 'unsafe-inline' in script-src erlauben. Sobald 'unsafe-inline' in script-src steht, ist der XSS-Schutz von CSP praktisch aufgehoben. Der Angreifer kann trotzdem beliebige inline-Scripts injizieren. Die korrekte Alternative sind Nonces oder Hashes. 'unsafe-inline' wird zudem ignoriert, wenn ein Nonce oder Hash in der Policy vorhanden ist, weshalb es bei nonce-basierten Policies keinen Effekt hat, aber trotzdem nicht stehen sollte.
Fehler 2: 'unsafe-eval' erlauben. eval(), Function() und verwandte Konstrukte sind häufige XSS-Vektoren. Wer 'unsafe-eval' in die Policy aufnimmt, öffnet eine Hintertür für Angreifer. Viele JavaScript-Bibliotheken sind ohne eval() einsetzbar. Prüfe die Anforderungen deiner Dependencies und versuche, auf eval-freie Alternativen umzusteigen.
Fehler 3: Statische Nonces wiederverwenden. Ein Nonce verliert seine Schutzwirkung, wenn er nicht bei jeder HTTP-Anfrage neu generiert wird. Ein Angreifer, der eine Seite mit einem gecachten Nonce in einem CDN oder Reverse Proxy sieht, kann ihn in seinen Angriffscode einfügen. Stelle sicher, dass kein CDN oder Reverse Proxy deine HTML-Antworten mit CSP-Nonces cachet. Setze Cache-Control: no-store für nonce-haltige Antworten.
Fehler 4: base-uri nicht einschränken. Ohne base-uri 'self' oder base-uri 'none' kann ein Angreifer ein <base href="https://evil.com">-Tag injizieren. Alle relativen URLs auf der Seite werden dann auf die Domain des Angreifers umgeleitet, was Skripte, Styles und Formulare betrifft. Dieser Angriff ist subtil und wird oft übersehen.
Fehler 5: JSONP-fähige Domains in script-src allowlisten. Wenn du eine Domain in script-src einträgst, die JSONP-Endpunkte anbietet (z. B. ältere Google-APIs, Social-Media-Widgets), kann ein Angreifer diese Endpunkte nutzen, um beliebigen JavaScript-Code einzuschleusen. Prüfe bei jeder erlaubten Domain, ob sie JSONP-Callbacks unterstützt. Die Lösung ist, stattdessen auf 'strict-dynamic' mit Nonces umzusteigen, das JSONP-Bypässe eliminiert.
Fehler 6: object-src und plugin-types vergessen. Flash-Plugins und Java-Applets sind längst obsolet, werden aber in älteren Browsern noch unterstützt und sind bekannte Exploit-Vektoren. object-src 'none' schließt diese Angriffsfläche vollständig.
Fehler 7: Policy zu permissiv starten und nie verschärfen. Viele Teams setzen eine breite Policy ein (“erst mal alles erlauben, damit nichts bricht”) und kommen nie dazu, sie zu verschärfen. Der richtige Ansatz ist umgekehrt: Mit einem maximalen Restrict starten, Report-Only nutzen und nur das freischalten, was wirklich gebraucht wird.
Troubleshooting: 8 häufige CSP-Probleme und ihre Lösungen
| Problem | Ursache | Lösung |
|---|---|---|
| Inline-Script wird blockiert trotz Nonce | Nonce im Header stimmt nicht mit Nonce im HTML überein (z. B. durch Caching) | Cache-Control: no-store für HTML-Antworten setzen, Nonce-Generierung debuggen |
| Externe Schriftart (Google Fonts) blockiert | font-src fehlt oder erlaubt fonts.gstatic.com nicht | font-src ‘self’ https://fonts.gstatic.com hinzufügen |
| WebSocket-Verbindung blockiert | wss:// in connect-src fehlt | connect-src ‘self’ wss://dein-server.de hinzufügen |
| React/Vue Build-Scripts blockiert | Gebaute Bundle-Files haben keine Nonce | strict-dynamic einsetzen oder Nonce via Template-Engine in index.html injizieren |
| Blob-URLs für Datei-Uploads blockiert | blob: in img-src fehlt | img-src ‘self’ data: blob: ergänzen |
| eval() in Third-Party-Library wirft CSP-Fehler | Bibliothek verwendet eval() oder Function()-Konstruktor | Bibliothek auf eval-freie Version upgraden oder ersetzen; als letztes Mittel unsafe-eval nur für src-Muster dieser Bibliothek |
| CSP-Header erscheint nicht in curl-Ausgabe | Middleware falsch platziert (nach dem ersten res.send() Aufruf) | Helmet/CSP-Middleware vor allen Routen platzieren, app.use() vor app.get() |
| Bilder von S3 oder CDN werden blockiert | img-src erlaubt nur ‘self’ | img-src ‘self’ https://dein-bucket.s3.eu-central-1.amazonaws.com hinzufügen |
| report-uri erhält keine Berichte | Content-Type des Reports nicht erkannt | express.json({ type: ‘application/csp-report’ }) als Middleware hinzufügen |
| Policy bricht Stripe/PayPal-Checkout | Payment-IFrames werden von frame-src blockiert | frame-src ‘self’ https://js.stripe.com oder entsprechende Payment-CDN-URL ergänzen |
Ein häufiges Debugging-Problem ist, dass der CSP-Header korrekt gesetzt ist, aber der Browser ihn ignoriert, weil eine meta http-equiv="Content-Security-Policy"-Direktive im HTML-Dokument einen anderen Wert setzt. Der Header hat Priorität vor dem Meta-Tag, aber wenn beide vorhanden sind, können unerwartete Konflikte entstehen. Entferne alle CSP-Meta-Tags und setze die Policy ausschließlich über den HTTP-Header.
Fortgeschrittene Tipps: Production-Ready CSP
Für eine produktionsreife CSP-Implementierung gibt es einige fortgeschrittene Muster, die über die Grundkonfiguration hinausgehen:
Differenzierte Policies für verschiedene Routen: Nicht alle Seiten einer Anwendung haben die gleichen Anforderungen. Eine Admin-Konsole braucht keine Payment-iFrames. Eine öffentliche Produktseite braucht keine WebSocket-Verbindungen. Setze route-spezifische CSP-Header über separate Middleware-Instanzen statt einer globalen Policy.
Reporting-API statt report-uri: Die modernere report-to-Direktive verwendet die Reporting API und unterstützt Gruppenreporting, Sampling-Rate und strukturiertere Reports. Für eine zukunftssichere Implementierung kombiniere beide:
// Reporting-Gruppe definieren
res.setHeader('Reporting-Endpoints', 'csp-endpoint="https://deine-app.de/csp-report"');
// CSP mit report-to und report-uri (Backward-Compat.)
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
`script-src 'nonce-${nonce}' 'strict-dynamic'`,
"object-src 'none'",
"base-uri 'none'",
"report-to csp-endpoint",
"report-uri /csp-report" // Fallback für ältere Browser
].join('; ')
);
CSP-Nonce mit Template-Engines (EJS, Pug, Handlebars): Template-Engines machen es einfach, den Nonce in alle Templates zu injizieren. Bei EJS übergibst du den Nonce als Template-Variable und verwendest ihn als <%- nonce %>. Das Wichtigste ist, dass alle Script- und Style-Tags den Nonce erhalten, auch die vom Framework generierten.
Sicherheitsaudit mit dem OWASP Secure Headers Project: Das OWASP Secure Headers Project pflegt eine Referenzliste empfohlener Header-Konfigurationen. Dort findest du aktuelle Empfehlungen für CSP, HSTS, X-Frame-Options und weitere Header, die du neben CSP setzen solltest.
Performance-Aspekte: CSP-Header haben keinen messbaren Performance-Einfluss auf die Serverseite, da es sich um einfache String-Operationen handelt. Der Browser muss jedoch die Policy parsen und bei jeder Ressourcenanforderung prüfen. Bei sehr langen Policies (z. B. viele allowlistete Domains) kann dies marginal länger dauern. Eine gut strukturierte Policy mit Nonces statt langen Domain-Listen ist daher auch aus Performance-Sicht vorzuziehen.
Vollständiges Beispielprojekt: Produktionsbereite CSP
Hier ist die vollständige, produktionsbereite server.js mit allen besprochenen Features:
const express = require('express');
const helmet = require('helmet');
const crypto = require('crypto');
const app = express();
const PORT = process.env.PORT || 3000;
const violations = [];
// 1. Nonce pro Request erzeugen
app.use((req, res, next) => {
res.locals.nonce = crypto.randomBytes(16).toString('base64');
next();
});
// 2. Helmet mit vollständiger CSP-Konfiguration
app.use((req, res, next) => {
const nonce = res.locals.nonce;
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: [
"'self'",
`'nonce-${nonce}'`,
"'strict-dynamic'"
],
styleSrc: ["'self'", `'nonce-${nonce}'`],
imgSrc: ["'self'", "data:", "blob:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
frameAncestors: ["'none'"],
baseUri: ["'none'"],
formAction: ["'self'"],
upgradeInsecureRequests: [],
reportUri: ['/csp-report']
}
})(req, res, next);
});
// 3. CSP Violation Reports empfangen
app.use(express.json({ type: ['application/json', 'application/csp-report'] }));
app.post('/csp-report', (req, res) => {
const report = req.body?.['csp-report'] || req.body;
if (report) {
const entry = {
time: new Date().toISOString(),
ip: req.ip,
violatedDirective: report['violated-directive'],
blockedUri: report['blocked-uri'],
documentUri: report['document-uri']
};
violations.push(entry);
// In Produktion: Logging-Service verwenden (z.B. Winston, Sentry)
console.warn('CSP-Verstoß:', entry);
}
res.status(204).end();
});
// 4. Hauptroute mit Nonce-Injection
app.get('/', (req, res) => {
const nonce = res.locals.nonce;
res.send(`
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSP Demo</title>
<style nonce="${nonce}">
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 2rem auto; padding: 1rem; }
.badge { background: #22c55e; color: white; padding: .25rem .75rem; border-radius: 9999px; font-size: .875rem; }
pre { background: #f1f5f9; padding: 1rem; border-radius: .5rem; overflow-x: auto; }
</style>
</head>
<body>
<h1>Content Security Policy <span class="badge">aktiv</span></h1>
<p>Diese Seite ist durch einen Nonce-basierten CSP-Header gesichert.</p>
<pre id="csp-output">CSP wird geladen...</pre>
<script nonce="${nonce}">
// Dieser inline-Script ist durch den Nonce erlaubt
fetch('/api/csp-info')
.then(r => r.json())
.then(data => {
document.getElementById('csp-output').textContent =
'Nonce: ' + data.nonce.substring(0, 10) + '... (erste 10 Zeichen)\\n' +
'Violations gesamt: ' + data.violations;
});
</script>
</body>
</html>
`);
});
// 5. API-Endpunkt für CSP-Status
app.get('/api/csp-info', (req, res) => {
res.json({
nonce: res.locals.nonce,
violations: violations.length
});
});
app.listen(PORT, () => {
console.log(`Server läuft auf Port ${PORT}`);
console.log(`CSP aktiv: Nonce-basiert mit strict-dynamic`);
});
Teste die Anwendung mit node server.js, öffne http://localhost:3000 und überprüfe den CSP-Header in den Browser-DevTools unter Netzwerk > Headers. Du siehst den vollständigen Header inklusive des frisch generierten Nonce-Werts, der bei jedem Reload wechselt.
Weiterführende Inhalte
Content Security Policy ist eine von mehreren Sicherheitsebenen für Node.js-Anwendungen. Diese Artikel bauen auf dem hier gezeigten Wissen auf oder ergänzen es:
- HTTP Security Headers in Node.js: 12 Schritte mit Helmet.js – X-Frame-Options, HSTS, X-Content-Type-Options und weitere Header
- CSRF Protection in Node.js: 12 Steps – Schutz vor Cross-Site Request Forgery als Ergänzung zu CSP
- OAuth 2.1 mit PKCE in Node.js: 12 Schritte – Sichere Authentifizierung für APIs und SPAs
- Rate Limiting in Node.js: 12 Steps, 30 Min – Brute-Force und DDoS-Schutz auf Anwendungsebene
- Node.js Session Management: 11 Steps, 30 Min – Sichere Session-Verwaltung in Express
- Two-Factor Authentication in Node.js: 11 Steps – TOTP-basierte 2FA implementieren
FAQ: Content Security Policy in Node.js
Macht CSP XSS-Schutz durch Input-Escaping überflüssig?
Nein. CSP und Input-Escaping sind komplementäre Maßnahmen. CSP ist eine Verteidigungsschicht im Browser und schlägt fehl, wenn ein Angreifer einen Bypass findet (z. B. über eine JSONP-fähige Domain in der allowlist). Input-Escaping verhindert, dass schädlicher Code überhaupt in die HTML-Ausgabe gelangt. Beide Maßnahmen zusammen (Defense in Depth) sind der richtige Ansatz.
Wie groß ist der Performance-Overhead durch CSP-Nonces?
crypto.randomBytes(16) ist eine sehr schnelle Operation auf modernen Server-CPUs und kostet weniger als 0,1 Millisekunde. Der Performance-Overhead durch CSP ist vernachlässigbar. Wichtiger ist, dass nonce-haltige HTML-Antworten nicht gecacht werden dürfen (Cache-Control: no-store), was caching-Strategien beeinflusst.
Kann ich CSP mit einem CDN wie Cloudflare kombinieren?
Ja, aber mit Vorsicht. Wenn Cloudflare oder ein anderes CDN HTML-Antworten cachet, werden gecachte Nonces an mehrere Nutzer ausgeliefert, was den Nonce-Schutz aufhebt. Die Lösung ist, HTML mit Nonces vom CDN auszunehmen (z. B. über Cache-Control: no-store auf der Origin oder einen Cache-Bypass für HTML-Antworten). Statische Assets (CSS, JS) können weiterhin gecacht werden.
Was passiert mit Browsern, die CSP nicht unterstützen?
CSP ist ein Opt-in-Sicherheitsmechanismus. Browser, die CSP nicht verstehen, ignorieren den Header und rendern die Seite normal. Das Verhalten der Seite ändert sich nicht, nur die Schutzwirkung fehlt. Da aber alle modernen Browser (Chrome ab Version 25, Firefox ab 23, Safari ab 10, Edge seit Beginn) CSP unterstützen, ist das 2026 kein praktisches Problem für die überwiegende Mehrheit der Nutzer.
Wie setze ich CSP für eine Next.js-Anwendung ein?
In Next.js setzt du CSP-Header entweder über next.config.js (statische Policy ohne Nonces) oder über benutzerdefinierte Server-Middleware bzw. middleware.ts für dynamische Nonce-basierte Policies. Bei Server-Side Rendering injizierst du den Nonce über getServerSideProps oder die neuen App Router Server Components. Die offizielle Next.js-Dokumentation beschreibt den empfohlenen Weg für beide Ansätze.
Welches Helmet.js CSP-Preset ist der beste Ausgangspunkt?
Helmet 8.x hat kein eingebautes “strict”-Preset für CSP. Der beste Ausgangspunkt ist die Konfiguration aus diesem Tutorial: default-src 'self', nonce-basiertes script-src, object-src 'none', base-uri 'none' und frame-ancestors 'none'. Diese 5 Direktiven decken die häufigsten Angriffsvektoren ab und entsprechen den aktuellen OWASP-Empfehlungen.
Wie debug ich CSP-Verstöße in der Produktion?
Richte einen /csp-report-Endpunkt ein wie in Schritt 8 gezeigt und leite die Reports in deinen Logging-Service (Sentry, Datadog, ELK Stack). Filtere bekannte False Positives heraus (Browser-Plugins, Passwort-Manager injizieren oft Scripts). Schalte zusätzlich temporär auf Content-Security-Policy-Report-Only um, wenn du eine Policy-Änderung testest, ohne die Produktion zu unterbrechen.
Reicht CSP allein für PCI-DSS- oder BSI-Compliance?
CSP ist eine wichtige Maßnahme, reicht aber nicht allein. PCI-DSS Version 4.0 (gültig ab März 2025) fordert unter Requirement 6.4.3 explizit, alle client-seitigen Scripts auf Zahlungsseiten zu kontrollieren, wobei CSP eine der akzeptierten Implementierungsmethoden ist. Für vollständige Compliance brauchst du zusätzlich Input-Validierung, sichere HTTP-Header (HSTS, X-Frame-Options), regelmäßige Penetrationstests und ein Vulnerability Management. Das BSI Grundschutz-Kompendium empfiehlt CSP als Teil des Web-Anwendungsschutzes ohne spezifische Versionsvorgaben.




