nonce-basierte Content Security Policy statt unsafe-inline
Eine Content Security Policy mit der Direktive unsafe-inline erlaubt jedes Inline-Skript auf der Seite auszuführen, egal ob es vom eigenen Team stammt oder von einem Angreifer per Cross-Site-Scripting eingeschleust wurde. Das ist keine Randnotiz, sondern hebelt den zentralen Schutzzweck einer CSP fast vollständig aus. CSP Level 3 löst dieses Dilemma mit einem pro Request neu generierten Nonce-Wert, der legitime Inline-Skripte gezielt freischaltet, während jedes eingeschleuste, nicht autorisierte Skript weiterhin blockiert bleibt.
Inhaltsverzeichnis
- 1. Das Problem mit unsafe-inline in der Praxis
- 2. Wie der Nonce-Mechanismus funktioniert
- 3. Sichere Nonce-Generierung: worauf es ankommt
- 4. strict-dynamic für moderne Single-Page-Apps
- 5. Fallback-Strategie für ältere Browser ohne strict-dynamic
- 6. Häufige Implementierungsfehler in der Praxis
- 7. Integration in serverseitiges Templating
- 8. Rollout-Strategie mit Report-Only-Modus
- 9. Fazit: Nonce-basierte CSP als Standard für moderne Anwendungen
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem mit unsafe-inline in der Praxis
Viele gewachsene Webanwendungen enthalten Inline-Skripte, sei es für Tracking-Snippets, dynamisch generierte Konfigurationswerte oder historisch gewachsenen Code, der nie in externe Dateien ausgelagert wurde. Um solche Seiten überhaupt mit einer Content Security Policy auszustatten, greifen Teams häufig zur schnellen Lösung script-src 'self' 'unsafe-inline', weil eine strengere Policy sonst sofort zahlreiche Konsolenfehler und kaputte Funktionen produziert.
Diese Abkürzung hat allerdings einen entscheidenden Nachteil: unsafe-inline erlaubt ausnahmslos jedes Inline-Skript, unabhängig davon, wer es eingefügt hat. Gelingt einem Angreifer eine Cross-Site-Scripting-Injektion, etwa durch eine unzureichend validierte Nutzereingabe, die ungefiltert ins HTML gelangt, führt der Browser das eingeschleuste Skript trotz aktiver CSP anstandslos aus. Die Policy schützt in diesem Fall nur auf dem Papier, in der Praxis bietet sie gegen die häufigste XSS-Angriffsform keinerlei Widerstand.
// Symfony-Middleware: Nonce pro Request generieren und in CSP-Header einsetzen
namespace App\EventListener;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpFoundation\RequestStack;
final class CspNonceListener
{
public function __construct(
private readonly RequestStack $requestStack,
) {
}
public function onKernelResponse(ResponseEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$nonce = $this->requestStack->getCurrentRequest()?->attributes->get('csp_nonce');
if ($nonce === null) {
return;
}
$policy = sprintf(
"script-src 'self' 'nonce-%s' 'strict-dynamic'; object-src 'none'; base-uri 'self';",
$nonce,
);
$event->getResponse()->headers->set('Content-Security-Policy', $policy);
}
}
2. Wie der Nonce-Mechanismus funktioniert
Ein Nonce ist ein zufällig generierter, kryptografisch ausreichend starker Einmalwert, der bei jeder einzelnen HTTP-Antwort neu erzeugt wird und niemals wiederverwendet werden darf. Der Server setzt diesen Wert sowohl in den CSP-Header als 'nonce-<wert>' innerhalb der script-src-Direktive als auch als nonce-Attribut auf jedes Inline-Skript-Tag, das ausgeführt werden soll.
Der Browser führt anschließend nur Inline-Skripte aus, deren nonce-Attribut exakt mit dem im Header angegebenen Wert übereinstimmt. Ein von einem Angreifer eingeschleustes Skript kennt diesen Wert nicht, denn er wird serverseitig bei jedem Request frisch generiert und ist für den Angreifer zum Zeitpunkt der Injektion nicht vorhersehbar. Selbst wenn die Injektion technisch gelingt, fehlt dem eingeschleusten Code das korrekte, aktülle Nonce-Attribut, und der Browser verweigert die Ausführung.
3. Sichere Nonce-Generierung: worauf es ankommt
Die Sicherheit des gesamten Mechanismus hängt vollständig davon ab, dass der Nonce-Wert mit einem kryptografisch sicheren Zufallsgenerator erzeugt wird, etwa random_bytes() in PHP oder das Web-Crypto-API im Browser-Kontext, niemals mit einer vorhersehbaren Funktion wie einem einfachen Zeitstempel oder einer inkrementellen Zahl. Ein vorhersagbarer Nonce hebelt den gesamten Schutz aus, weil ein Angreifer ihn dann in sein eingeschleustes Skript-Tag einfach mit einsetzen könnte.
Ebenso wichtig ist, den Nonce-Wert wirklich bei jedem einzelnen Request neu zu generieren und niemals über mehrere Antworten hinweg wiederzuverwenden, etwa durch Caching der kompletten HTML-Antwort inklusive des darin eingebetteten Nonce-Werts. Ein per Full-Page-Cache mehrfach ausgelieferter, identischer Nonce-Wert wäre für alle Nutzer gleich und liesse sich von einem Angreifer, der die Seite einmal selbst aufruft, direkt auslesen und wiederverwenden.
4. strict-dynamic für moderne Single-Page-Apps
Moderne JavaScript-Anwendungen laden häufig zur Laufzeit weitere Skripte nach, etwa über dynamische Imports oder von einem Hauptbundle nachgeladene Chunk-Dateien. Eine klassische, allowlist-basierte CSP müsste jede dieser nachgeladenen Skript-URLs einzeln kennen und explizit erlauben, was bei modernem Code-Splitting schnell unpraktikabel wird und bei jedem neuen Chunk-Namen zu Konsolenfehlern führt.
Die Direktive strict-dynamic löst dieses Problem, indem sie das Vertrauen, das einem per Nonce autorisierten Skript entgegengebracht wird, automatisch auf alle Skripte ausdehnt, die dieses Skript selbst zur Laufzeit per createElement und Anhängen ans DOM nachlädt. Klassische Allowlist-Direktiven wie 'self' werden bei aktivem strict-dynamic von unterstützenden Browsern ignoriert, was in der Praxis bedeutet, dass eine Kombination aus Nonce und strict-dynamic sowohl maximale Sicherheit als auch praktische Kompatibilität mit modernem Bundling bietet.
5. Fallback-Strategie für ältere Browser ohne strict-dynamic
Da nicht jeder Browser strict-dynamic unterstützt, empfiehlt die CSP-Spezifikation selbst eine mehrschichtige Direktive, bei der ältere Browser eine klassische Allowlist wie 'self' https://trusted-cdn.example respektieren, während moderne Browser dieselbe Direktive dank strict-dynamic ignorieren und stattdessen dem Nonce-Mechanismus folgen. Diese Kombination funktioniert, weil CSP-fähige, aber ältere Browser unbekannte Schlüsselwörter wie strict-dynamic stillschweigend ignorieren, während moderne Browser genau umgekehrt reagieren.
In der Praxis sieht eine solche kombinierte Direktive etwa so aus: script-src 'nonce-abc123' 'strict-dynamic' 'self' https://trusted-cdn.example. Ältere Browser lesen diese Zeile als klassische Allowlist mit Nonce-Ausnahme, moderne Browser aktivieren dank strict-dynamic den dynamischen Vertrauensmechanismus und ignorieren die URL-basierte Allowlist komplett. So bleibt die Seite in beiden Fällen funktionsfähig, mit stark unterschiedlichem, aber jeweils bestmöglichem Sicherheitsniveau.
6. Häufige Implementierungsfehler in der Praxis
Ein häufiger Fehler ist ein Nonce-Wert, der zwar zufällig generiert wird, aber über einen globalen Anwendungscache oder eine Middleware-Konfiguration versehentlich für mehrere Requests wiederverwendet wird, weil die Generierung nicht wirklich pro Request, sondern nur beim Anwendungsstart einmalig erfolgt. Ein solcher Fehler fällt in Tests oft nicht auf, weil die CSP technisch weiterhin funktioniert, das eigentliche Sicherheitsversprechen aber vollständig entfällt.
Ein zweiter häufiger Fehler ist das Setzen des CSP-Headers ausschließlich für die Haupt-HTML-Antwort, während dynamisch nachgeladene Inhalte, etwa per AJAX nachgeladene Teilseiten oder serverseitig gerenderte Fragmente, ohne eigene, konsistente Nonce-Zuordnung ausgeliefert werden. Gerade bei serverseitig gerenderten Komponenten, die unabhängig vom Hauptrequest gecacht werden, muss der Nonce-Mechanismus sorgfältig durchdacht werden, um Inkonsistenzen zwischen Header und eingebetteten Skripten zu vermeiden.
7. Integration in serverseitiges Templating
Damit der Nonce-Wert konsistent im gesamten HTML-Dokument verwendet wird, muss er einmal pro Request generiert und danach an jeder Stelle referenziert werden können, an der ein Inline-Skript im Template steht. In Symfony bietet sich dafür ein Request-Attribut an, das früh im Kernel-Request-Zyklus gesetzt wird und anschließend sowohl im Twig-Template als auch im Response-Listener für den CSP-Header verfügbar ist.
Wichtig ist dabei, jedes einzelne Inline-Skript-Tag im Template konsequent mit dem nonce-Attribut zu versehen, denn ein einziges vergessenes Tag führt zu einem funktional kaputten Skript und einem entsprechenden Konsolenfehler, nicht zu einer stillen Sicherheitslücke. Diese Fail-Closed-Eigenschaft ist im Vergleich zu unsafe-inline ein wesentlicher Vorteil, weil Fehler sofort sichtbar werden, statt unbemerkt zu bleiben.
8. Rollout-Strategie mit Report-Only-Modus
Der Umstieg von unsafe-inline auf einen nonce-basierten Ansatz sollte in bestehenden, gewachsenen Anwendungen nicht abrupt erfolgen, sondern über den Header Content-Security-Policy-Report-Only schrittweise getestet werden. In diesem Modus meldet der Browser Verstösse gegen die Policy, ohne sie tatsächlich zu blockieren, was einen risikofreien Testlauf in Produktion ermöglicht, bevor die Policy scharf geschaltet wird.
Ein Reporting-Endpunkt, konfiguriert über die Direktive report-to, sammelt während dieser Testphase alle Verstösse und zeigt zuverlässig, welche Inline-Skripte im Code noch kein Nonce-Attribut besitzen. Erst wenn die Verstossmeldungen über einen ausreichend langen Zeitraum auf null zurückgehen, sollte die Policy vom Report-Only-Modus in den scharfen, blockierenden Modus überführt werden.
9. Fazit: Nonce-basierte CSP als Standard für moderne Anwendungen
Eine Content Security Policy mit unsafe-inline ist in der Praxis kaum mehr als eine formale Geste, weil sie genau die Angriffsklasse offen lässt, gegen die eine CSP eigentlich schützen soll. Der nonce-basierte Ansatz aus CSP Level 3 löst dieses Dilemma, indem er legitime Inline-Skripte gezielt und pro Request autorisiert, während eingeschleuster Code ohne gültiges Nonce zuverlässig blockiert bleibt.
Kombiniert mit strict-dynamic für modernes Code-Splitting und einer schrittweisen Einführung über den Report-Only-Modus lässt sich dieser Ansatz auch in gewachsenen Anwendungen ohne großes Risiko einführen. Wer eine CSP wirklich als Schutzmaßnahme und nicht nur als Compliance-Haken versteht, kommt am nonce-basierten Modell auf lange Sicht kaum vorbei.
| Ansatz | Schutz gegen XSS | Wartungsaufwand | Kompatibilität |
|---|---|---|---|
| unsafe-inline | Praktisch kein Schutz | Sehr gering | Universell |
| Allowlist per Domain | Begrenzt, umgehbar bei JSONP-Endpunkten | Hoch bei vielen Quellen | Gut, aber unflexibel bei Code-Splitting |
| Nonce-basiert | Stark, pro Request zufällig | Mittel, Templating-Integration nötig | Alle modernen Browser |
| Nonce + strict-dynamic | Stark, deckt dynamisches Nachladen ab | Mittel, einmalige Einrichtung | Moderne Browser, mit Fallback für ältere |
Mironsoft
Security-Audits, OWASP-konforme Härtung und sichere Architektur
Anwendungen, die einem echten Angriffsversuch tatsächlich standhalten?
Wir prüfen bestehende Anwendungen auf klassische OWASP-Schwachstellen, unsichere Authentifizierung und fehlende Input-Validierung und bauen daraus eine Architektur, die Angriffsflächen strukturell reduziert statt nur einzelne Symptome zu flicken.
Security-Audit
OWASP Top 10, Auth-Flows und Input-Validierung systematisch auf Schwachstellen prüfen.
Sichere Architektur
Rate-Limiting, Verschlüsselung und Zugriffskontrollen von Grund auf richtig aufbauen.
Incident-Vorbereitung
Logging, Monitoring und Reaktionsprozesse für den Ernstfall etablieren.
10. Zusammenfassung
Nonce-basierte CSP nach Level 3: Das Wichtigste auf einen Blick
Kernproblem
unsafe-inline erlaubt jedes Inline-Skript, auch eingeschleusten XSS-Code.
Lösung
Ein pro Request neu generierter Nonce-Wert autorisiert gezielt legitime Skripte.
Für SPAs
strict-dynamic dehnt das Vertrauen auf dynamisch nachgeladene Skripte aus.
Einführung
Report-Only-Modus deckt fehlende Nonce-Attribute vor dem scharfen Rollout auf.