CSP Level 3: nonce-basierte Content Security Policy statt unsafe-inline
AI generated
OWASP
0x00
Security · CSP · Browser-Header · XSS-Schutz
CSP Level 3
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.

16 Min. Lesezeit CSP Level 3 · Nonce strict-dynamic · XSS-Schutz

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.

11. FAQ: Nonce-basierte CSP nach Level 3: Das Wichtigste auf einen Blick

1Warum ist unsafe-inline in einer CSP problematisch?
Weil die Direktive ausnahmslos jedes Inline-Skript ausführen lässt, unabhängig von seiner Herkunft. Ein per Cross-Site-Scripting eingeschleustes Skript wird trotz aktiver CSP ganz normal ausgeführt.
2Was genau ist ein CSP-Nonce?
Ein zufällig generierter, kryptografisch sicherer Einmalwert, der bei jeder HTTP-Antwort neu erzeugt und sowohl im CSP-Header als auch als Attribut auf erlaubten Inline-Skripten hinterlegt wird.
3Warum darf ein Nonce nicht wiederverwendet werden?
Ein wiederverwendeter, vorhersehbarer Nonce liesse sich von einem Angreifer auslesen und in ein eingeschleustes Skript-Tag einsetzen, wodurch der gesamte Schutzmechanismus wirkungslos würde.
4Was macht strict-dynamic konkret?
Es dehnt das Vertrauen eines per Nonce autorisierten Skripts automatisch auf alle Skripte aus, die dieses Skript selbst zur Laufzeit nachlädt, ohne dass jede nachgeladene URL einzeln in der Policy stehen muss.
5Funktioniert strict-dynamic in allen Browsern?
Nein, ältere Browser ignorieren das unbekannte Schlüsselwort und respektieren stattdessen eine parallel angegebene, klassische Allowlist, wodurch ein funktionierender Fallback entsteht.
6Wie wird der Nonce-Wert generiert?
Mit einem kryptografisch sicheren Zufallsgenerator wie random_bytes() in PHP, niemals mit einer vorhersehbaren Quelle wie einem Zeitstempel oder einer fortlaufenden Nummer.
7Kann ein Full-Page-Cache den Nonce-Mechanismus brechen?
Ja, wenn die komplette HTML-Antwort inklusive Nonce zwischengespeichert wird, erhalten alle Nutzer denselben Wert, was den Schutz aushebelt. Der Nonce muss bei jeder Auslieferung frisch generiert werden.
8Was ist der Report-Only-Modus?
Ein CSP-Modus, in dem der Browser Verstösse gegen die Policy meldet, ohne sie zu blockieren. Er eignet sich, um vor der scharfen Einführung zu prüfen, welche Inline-Skripte noch kein Nonce-Attribut haben.
9Was passiert, wenn ein Inline-Skript im Template das Nonce-Attribut vergisst?
Der Browser blockiert dieses Skript und meldet einen Konsolenfehler, das Skript läuft schlicht nicht. Der Fehler ist sofort sichtbar, statt unbemerkt eine Sicherheitslücke offen zu lassen.
10Ist eine nonce-basierte CSP auch für klassische serverseitige Anwendungen sinnvoll?
Ja, sobald irgendwo Inline-Skripte im Template stehen. Serverseitiges Templating wie Twig oder Blade kann den Nonce-Wert problemlos einmal pro Request generieren und konsistent einsetzen.