Zurück zu allen Beiträgen
·5 Min. Lesezeit

Content Security Policy gegen XSS: melden, verschärfen, durchsetzen

Die erste Verteidigungslinie gegen XSS ist immer, Eingaben korrekt zu verarbeiten und Ausgaben korrekt zu kodieren. Doch so sorgfältig man auch arbeitet: Irgendwann kann ein übersehenes innerHTML, ein Skript eines Drittanbieters oder ein Abhängigkeits-Update diese Linie durchbrechen. Content Security Policy (CSP) ist eine zweite Schicht, die dem Browser sagt, aus welchen Quellen auf einer Seite Code laufen darf. Richtig aufgesetzt verhindert sie, dass ein eingeschleustes Skript ausgeführt wird.

Der schwierige Teil bei CSP ist, sie in eine bestehende Anwendung einzubauen, ohne etwas zu brechen. In diesem Beitrag beschreibe ich den Ablauf: die Policy zuerst im Berichtsmodus ausliefern, die Verstöße auswerten, auf eine Nonce-basierte Policy umstellen und sie dann durchsetzen.

Was CSP leistet und was nicht

CSP ist ein HTTP-Antwort-Header. Der Browser prüft Quellen für Skripte, Styles, Bilder, Verbindungen und Frames anhand der Direktiven darin. Eine nicht erlaubte Quelle wird nicht geladen oder nicht ausgeführt.

Die Grenze, die man kennen sollte: CSP schließt die Schwachstelle nicht, sie erschwert die Ausnutzung. Die Ausgabekodierung bleibt nötig. Man kann CSP als Sicherheitsgurt sehen, der zählt, wenn diese Verteidigung versagt. Sie ist außerdem keine Lösung für serverseitige Autorisierungsprobleme, CSRF oder SQL-Injection.

Warum eine Allowlist nicht reicht

Die erste Idee ist meist, vertrauenswürdige Hosts aufzulisten:

Content-Security-Policy: script-src 'self' https://cdn.beispiel.de

Das wirkt vernünftig, ist in der Praxis aber fragil. Jedes Skript auf einem erlaubten Host, etwa ein offener JSONP-Endpunkt oder eine alte Bibliothek, kann einem Angreifer ausführbaren Code liefern. Je länger die Liste wird, desto schwerer ist sie zu pflegen. Deshalb lautet die heutige Empfehlung, sich statt auf Hosts auf eine pro Antwort erzeugte Nonce oder auf Skript-Hashes zu stützen.

Eine Nonce-basierte Policy

Die Idee ist einfach: Der Server erzeugt pro Anfrage einen Zufallswert und schreibt ihn sowohl in den Header als auch in die vertrauenswürdigen script-Tags. Der Browser führt nur Skripte aus, die diese Nonce tragen.

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{ZUFALL}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'none'

In ASP.NET Core lässt sich das mit einer Middleware umsetzen:

app.Use(async (context, next) =>
{
    var nonce = Convert.ToBase64String(RandomNumberGenerator.GetBytes(16));
    context.Items["csp-nonce"] = nonce;

    context.Response.Headers["Content-Security-Policy"] =
        $"default-src 'self'; " +
        $"script-src 'nonce-{nonce}' 'strict-dynamic'; " +
        "object-src 'none'; base-uri 'none'; frame-ancestors 'none'";

    await next();
});

Der entscheidende Punkt: Die Nonce muss bei jeder Anfrage neu erzeugt werden und unvorhersehbar sein. Ein fester Wert oder die Wiederverwendung derselben Nonce, weil die Seite gecacht wird, hebt den Schutz auf. Wenn Sie Full-Page-Caching nutzen, ziehen Sie statt Nonces einen Hash-basierten Ansatz in Betracht.

'strict-dynamic' überträgt das Vertrauen von einem per Nonce erlaubten Skript auf die Skripte, die es nachlädt. So müssen Sie nicht für jede Datei, die ein Bundler dynamisch lädt, eine eigene Erlaubnis schreiben.

Zuerst Report-Only: lernen, ohne zu brechen

Wenn Sie die Policy sofort durchsetzen, kann ein vergessenes Inline-Skript in Produktion einen Button lahmlegen. Veröffentlichen Sie stattdessen dieselbe Policy mit dem Header Content-Security-Policy-Report-Only. Der Browser blockiert Verstöße nicht, er meldet sie nur.

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'nonce-{ZUFALL}' 'strict-dynamic';
  report-to csp-endpoint

Der Report-Endpunkt kann ein einfacher POST-Empfänger sein. Schreiben Sie die Berichte in eine Tabelle und prüfen Sie, welche Direktive verletzt wurde, auf welcher Seite und durch welche Quelle. Nach einigen Tagen oder einer Woche Daten filtern Sie das typische Rauschen heraus: Verstöße durch Browser-Erweiterungen, Inline-Event-Handler auf einer alten Seite, ein vergessenes Analytics-Skript.

Schützen Sie den Report-Endpunkt mit Ratenbegrenzung. Er ist öffentlich und nicht authentifiziert, sodass jemand mit schlechten Absichten ihn mit gefälschten Berichten fluten könnte.

Inline-Code aufräumen

Die Berichte zeigen meist auf dieselben drei Dinge:

  • Inline-Event-Handler wie onclick="...". Verschieben Sie sie mit addEventListener in eine Skriptdatei.
  • Inline-<script>-Blöcke. Wenn sie wirklich nötig sind, fügen Sie eine Nonce hinzu, andernfalls lagern Sie sie in eine externe Datei aus.
  • Bibliotheken, die eval oder new Function verwenden. Sie erfordern 'unsafe-eval'; ersetzen Sie die Bibliothek, wenn möglich, oder prüfen Sie ihre Konfiguration.

Bei Styles sind Inline-style-Attribute weniger riskant, lassen sich aber nach derselben Logik behandeln. In der ersten Phase ist es eine realistische Priorität, Skripte zu verschärfen und die Style-Policy auf später zu verschieben; Skriptausführung ist ein deutlich ernsteres Risiko als Style-Injection.

Der Wechsel zur Durchsetzung

Wenn der Berichtsstrom ruhiger geworden ist, ändern Sie den Header in Content-Security-Policy. Machen Sie den Wechsel schrittweise: zuerst für einen kleinen Prozentsatz des Traffics oder nur für interne Nutzer, dann für alle. Steuern Sie das über ein Feature-Flag, können Sie bei Problemen mit einem Schritt zurückgehen.

Lassen Sie Report-Only nach der Durchsetzung nicht ganz fallen. Wenn Sie eine neue Policy-Version (zum Beispiel ein strengeres style-src) in Report-Only testen, können Sie beide Header zusammen senden und weiterhin die aktuelle Policy durchsetzen. Sie arbeiten unabhängig voneinander.

Häufige Fehler

  • 'unsafe-inline' hinzufügen. Es löst das Problem schnell, nimmt CSP für Skripte aber weitgehend ihren Wert. Ist eine Nonce vorhanden, ignorieren moderne Browser 'unsafe-inline' ohnehin; zählen Sie es trotzdem nicht als Sicherheitsmechanismus.
  • Die Nonce cachen. Wird dasselbe HTML mit derselben Nonce an verschiedene Nutzer ausgeliefert, wird die Nonce vorhersehbar.
  • object-src und base-uri vergessen. Fehlen diese beiden, kann es trotz der Skript-Einschränkung möglich sein, Code auf anderem Weg auszuführen.
  • Die Berichte nie lesen. Wird der Berichtsstrom eingerichtet, aber nicht beobachtet, veraltet CSP unbemerkt.
  • Eine Policy für alles. Haben das Admin-Panel und die öffentlichen Seiten unterschiedliche Anforderungen, kann es sinnvoll sein, je Pfad unterschiedliche Policies zu erzeugen.

Fazit

CSP ist kein Header, den man einmal schreibt und vergisst, sondern ein Vertrag, der mit der Anwendung lebt. Zuerst mit Report-Only beobachten, Inline-Code aufräumen, die Policy mit Nonce und strict-dynamic verschärfen und dann schrittweise durchsetzen. Sie ersetzt die Ausgabekodierung nicht, kann aber die Folge einer durchgerutschten Injection auf „das Skript lief nicht und ein Bericht kam an" reduzieren. So wird aus einem Vorfall ein beherrschbarer Hinweis.