Cross-Site-Scripting (XSS) mit einer strengen Content Security Policy (CSP) abschwächen

Lukas Weichselbaum
Lukas Weichselbaum

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

Cross-Site-Scripting (XSS), also die Möglichkeit, schädliche Skripts in eine Webanwendung einzuschleusen, ist seit über einem Jahrzehnt eine der größten Sicherheitslücken im Web.

Die Content Security Policy (CSP) ist eine zusätzliche Sicherheitsebene, die dazu beiträgt, XSS-Angriffe zu minimieren. Wenn Sie eine CSP konfigurieren möchten, fügen Sie einer Webseite den HTTP-Header Content-Security-Policy hinzu und legen Sie Werte fest, die steuern, welche Ressourcen der User-Agent für diese Seite laden kann.

Auf dieser Seite wird erläutert, wie Sie eine CSP basierend auf Nonces oder Hashes verwenden, um XSS-Angriffe zu minimieren, anstatt die häufig verwendeten CSPs auf Basis von Host-Zulassungslisten zu verwenden, die die Seite oft XSS-Angriffen aussetzen, da sie in den meisten Konfigurationen umgangen werden können.

Schlüsselbegriff: Eine Nonce ist eine Zufallszahl, die nur einmal verwendet wird und mit der Sie ein <script>-Tag als vertrauenswürdig kennzeichnen können.

Wichtiger Begriff: Eine Hash-Funktion ist eine mathematische Funktion, die einen Eingabewert in einen komprimierten numerischen Wert umwandelt, der als Hash bezeichnet wird. Sie können einen Hash (z. B. SHA-256) verwenden, um ein Inline-<script>-Tag als vertrauenswürdig zu kennzeichnen.

Eine Content Security Policy, die auf Nonces oder Hashes basiert, wird oft als strikte CSP bezeichnet. Wenn eine Anwendung eine strenge CSP verwendet, können Angreifer, die HTML-Injektionsfehler finden, diese in der Regel nicht nutzen, um den Browser zu zwingen, schädliche Skripts in einem anfälligen Dokument auszuführen. Das liegt daran, dass eine strikte CSP nur gehashte Skripts oder Skripts mit dem korrekten Nonce-Wert zulässt, der auf dem Server generiert wurde. Angreifer können das Skript also nicht ausführen, ohne die korrekte Nonce für eine bestimmte Antwort zu kennen.

Warum sollten Sie eine strenge CSP verwenden?

Wenn Ihre Website bereits eine CSP wie script-src www.googleapis.com hat, ist sie wahrscheinlich nicht wirksam gegen Cross-Site-Scripting. Diese Art von CSP wird als Zulassungslisten-CSP bezeichnet. Sie erfordern viele Anpassungen und können von Angreifern umgangen werden.

Strikte CSPs, die auf kryptografischen Nonces oder Hashes basieren, vermeiden diese Fallstricke.

Strikte CSP-Struktur

Eine einfache strikte Content Security Policy verwendet einen der folgenden HTTP-Antwortheader:

Nonce-basierte strikte CSP

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
Funktionsweise einer Nonce-basierten strikten CSP

Hash-basierte strikte CSP

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Die folgenden Eigenschaften machen eine CSP wie diese „strikt“ und daher sicher:

  • Dabei werden Nonces 'nonce-{RANDOM}' oder Hashes 'sha256-{HASHED_INLINE_SCRIPT}' verwendet, um anzugeben, welchen <script>-Tags der Entwickler der Website vertraut, damit sie im Browser des Nutzers ausgeführt werden.
  • Dadurch wird 'strict-dynamic' festgelegt, um den Aufwand für die Bereitstellung einer Nonce- oder Hash-basierten CSP zu reduzieren. Dazu wird die Ausführung von Scripts, die von einem vertrauenswürdigen Script erstellt werden, automatisch zugelassen. Außerdem wird die Verwendung der meisten JavaScript-Bibliotheken und Widgets von Drittanbietern ermöglicht.
  • Sie basiert nicht auf URL-Zulassungslisten und ist daher nicht von häufigen CSP-Umgehungen betroffen.
  • Sie blockiert nicht vertrauenswürdige Inline-Scripts wie Inline-Ereignis-Handler oder javascript:-URIs.
  • Sie schränkt object-src ein, um gefährliche Plug-ins wie Flash zu deaktivieren.
  • Sie schränkt base-uri ein, um das Einfügen von <base>-Tags zu blockieren. Dadurch wird verhindert, dass Angreifer die Speicherorte von Skripts ändern, die von relativen URLs geladen werden.

Strikte CSP verwenden

Wenn Sie eine strikte CSP einführen möchten, müssen Sie Folgendes tun:

  1. Entscheiden Sie, ob Ihre Anwendung eine Nonce- oder Hash-basierte CSP festlegen soll.
  2. Kopieren Sie die CSP aus dem Abschnitt Strikte CSP-Struktur und legen Sie sie als Antwortheader für Ihre gesamte Anwendung fest.
  3. Refaktorieren Sie HTML-Vorlagen und clientseitigen Code, um Muster zu entfernen, die nicht mit der CSP kompatibel sind.
  4. Stellen Sie Ihre CSP bereit.

Mit der Best Practices-Prüfung von Lighthouse (Version 7.3.0 und höher mit dem Flag --preset=experimental) können Sie während dieses Prozesses prüfen, ob Ihre Website eine CSP hat und ob diese streng genug ist, um XSS-Angriffe zu verhindern.

Lighthouse-Bericht mit der Warnung, dass im erzwungenen Modus keine CSP gefunden wurde.
Wenn Ihre Website keine CSP hat, wird in Lighthouse diese Warnung angezeigt.

Schritt 1: Entscheiden, ob Sie eine Nonce- oder Hash-basierte CSP benötigen

So funktionieren die beiden Arten von strikter CSP:

Nonce-basierte CSP

Bei einer Nonce-basierten CSP generieren Sie zur Laufzeit eine Zufallszahl, fügen sie in Ihre CSP ein und verknüpfen sie mit jedem Script-Tag auf Ihrer Seite. Ein Angreifer kann kein schädliches Script in deine Seite einfügen oder ausführen, da er die richtige Zufallszahl für dieses Script erraten müsste. Das funktioniert nur, wenn die Zahl nicht erraten werden kann und zur Laufzeit für jede Antwort neu generiert wird.

Verwenden Sie eine Nonce-basierte CSP für HTML-Seiten, die auf dem Server gerendert werden. Für diese Seiten können Sie für jede Antwort eine neue Zufallszahl erstellen.

Hash-basierte CSP

Bei einer hashbasierten CSP wird der Hash jedes Inline-Script-Tags der CSP hinzugefügt. Jedes Script hat einen anderen Hash. Ein Angreifer kann kein schädliches Skript in Ihre Seite einfügen oder ausführen, da der Hash dieses Skripts in Ihrer CSP enthalten sein müsste, damit es ausgeführt werden kann.

Verwenden Sie eine hashbasierte CSP für statisch bereitgestellte HTML-Seiten oder Seiten, die im Cache gespeichert werden müssen. Sie können beispielsweise eine hashbasierte CSP für einseitige Webanwendungen verwenden, die mit Frameworks wie Angular oder React erstellt wurden und statisch ohne serverseitiges Rendering bereitgestellt werden.

Schritt 2: Strikte CSP festlegen und Skripts vorbereiten

Beim Festlegen einer CSP haben Sie mehrere Möglichkeiten:

  • Modus „Nur Berichterstellung“ (Content-Security-Policy-Report-Only) oder erzwungener Modus (Content-Security-Policy). Im Modus „Nur Berichterstellung“ blockiert die CSP noch keine Ressourcen, sodass auf Ihrer Website nichts kaputt geht. Sie können jedoch Fehler sehen und Berichte für alles erhalten, was blockiert worden wäre. Wenn ihr eure CSP lokal festlegt, spielt das keine Rolle, da beide Modi die Fehler in der Browserkonsole anzeigen. Der erzwungene Modus kann Ihnen helfen, Ressourcen zu finden, die durch Ihren CSP-Entwurf blockiert werden, da das Blockieren einer Ressource dazu führen kann, dass Ihre Seite fehlerhaft aussieht. Der Modus „Nur Berichte“ ist später im Prozess am nützlichsten (siehe Schritt 5).
  • Header oder HTML-<meta>-Tag. Für die lokale Entwicklung kann ein <meta>-Tag praktischer sein, um die CSP anzupassen und schnell zu sehen, wie sie sich auf Ihre Website auswirkt. Beachte jedoch Folgendes:
    • Wenn Sie Ihre CSP später in der Produktion bereitstellen, empfehlen wir, sie als HTTP-Header festzulegen.
    • Wenn Sie Ihre CSP im Nur-Berichtsmodus festlegen möchten, müssen Sie sie als Header festlegen, da CSP-Meta-Tags den Nur-Berichtsmodus nicht unterstützen.

Option A: Nonce-basierte CSP

Legen Sie in Ihrer Anwendung den folgenden Content-Security-Policy-HTTP-Antwortheader fest:

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Nonce für CSP generieren

Eine Nonce ist eine Zufallszahl, die nur einmal pro Seitenaufbau verwendet wird. Eine Nonce-basierte CSP kann XSS nur dann abwehren, wenn Angreifer den Nonce-Wert nicht erraten können. Eine CSP-Nonce muss:

  • Ein kryptografisch starker Zufallswert (idealerweise mindestens 128 Bit lang)
  • Für jede Antwort neu generiert
  • Base64-codiert

Hier sind einige Beispiele dafür, wie Sie eine CSP-Nonce in serverseitigen Frameworks hinzufügen:

const app = express();

app.get('/', function(request, response) {
  // Generate a new random nonce value for every response.
  const nonce = crypto.randomBytes(16).toString("base64");

  // Set the strict nonce-based CSP response header
  const csp = `script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none';`;
  response.set("Content-Security-Policy", csp);

  // Every <script> tag in your application should set the `nonce` attribute to this value.
  response.render(template, { nonce: nonce });
});

Fügen Sie <script>-Elementen ein nonce-Attribut hinzu.

Bei einer Nonce-basierten CSP muss jedes <script>-Element ein nonce-Attribut haben, das mit dem im CSP-Header angegebenen zufälligen Nonce-Wert übereinstimmt. Alle Skripts können dieselbe Nonce haben. Im ersten Schritt fügen Sie diese Attribute allen Skripts hinzu, damit sie von der CSP zugelassen werden.

Option B: Hash-basierter CSP-Antwortheader

Legen Sie in Ihrer Anwendung den folgenden Content-Security-Policy-HTTP-Antwortheader fest:

Content-Security-Policy:
  script-src 'sha256-{HASHED_INLINE_SCRIPT}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';

Für mehrere Inline-Skripts lautet die Syntax so: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.

Skripts aus Quellen dynamisch laden

Sie können Drittanbieterskripts dynamisch mit einem Inline-Skript laden.

Beispiel für das Inline-Einbinden von Skripts
Durch CSP zulässig
<script>
  var scripts = [ 'https://example.org/foo.js', 'https://example.org/bar.js'];

  scripts.forEach(function(scriptUrl) {
    var s = document.createElement('script');
    s.src = scriptUrl;
    s.async = false; // to preserve execution order
    document.head.appendChild(s);
  });
</script>
Damit dieses Script ausgeführt werden kann, müssen Sie den Hash des Inline-Scripts berechnen und ihn dem CSP-Antwortheader hinzufügen. Ersetzen Sie dabei den Platzhalter {HASHED_INLINE_SCRIPT}. Wenn Sie die Anzahl der Hashes reduzieren möchten, können Sie alle Inline-Skripts in einem einzigen Skript zusammenfassen. Ein Beispiel dafür finden Sie hier und den zugehörigen Code hier.
Durch CSP blockiert
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
Die CSP blockiert diese Skripts, weil sie nicht dynamisch hinzugefügt wurden und kein integrity-Attribut haben, das mit einer zulässigen Quelle übereinstimmt.

Überlegungen zum Laden von Skripts

Im Beispiel für das Inline-Script wird s.async = false hinzugefügt, um sicherzustellen, dass foo vor bar ausgeführt wird, auch wenn bar zuerst geladen wird. In diesem Snippet blockiert s.async = false den Parser nicht, während die Skripts geladen werden, da die Skripts dynamisch hinzugefügt werden. Der Parser wird nur während der Ausführung der Skripts angehalten, wie es bei async-Skripts der Fall wäre. Beachten Sie jedoch Folgendes:

  • Eines oder beide Skripts werden möglicherweise ausgeführt, bevor das Dokument vollständig heruntergeladen wurde. Wenn das Dokument bereit sein soll, wenn die Skripts ausgeführt werden, warten Sie auf das DOMContentLoaded-Ereignis, bevor Sie die Skripts anhängen. Wenn dies zu einem Leistungsproblem führt, weil die Scripts nicht früh genug heruntergeladen werden, verwenden Sie weiter oben auf der Seite Preload-Tags.
  • defer = true tut nichts. Wenn Sie dieses Verhalten benötigen, führen Sie das Skript bei Bedarf manuell aus.

Schritt 3: HTML-Vorlagen und clientseitigen Code refaktorieren

Inline-Ereignis-Handler (z. B. onclick="…", onerror="…") und JavaScript-URIs (<a href="javascript:…">) können zum Ausführen von Skripts verwendet werden. Das bedeutet, dass ein Angreifer, der einen XSS-Programmfehler findet, diese Art von HTML einschleusen und schädliches JavaScript ausführen kann. Eine Nonce- oder Hash-basierte CSP verbietet die Verwendung dieser Art von Markup. Wenn auf Ihrer Website eines dieser Muster verwendet wird, müssen Sie es durch eine sicherere Alternative ersetzen.

Wenn Sie die CSP im vorherigen Schritt aktiviert haben, werden CSP-Verstöße in der Konsole angezeigt, wenn die CSP ein inkompatibles Muster blockiert.

CSP-Verstoßberichte in der Chrome-Entwicklerkonsole.
Konsolenfehler für blockierten Code.

In den meisten Fällen lässt sich das Problem aber schnell beheben:

Inline-Event-Handler refaktorieren

Durch CSP zulässig
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
Die CSP lässt Event-Handler zu, die mit JavaScript registriert wurden.
Durch CSP blockiert
<span onclick="doThings();">A thing.</span>
CSP blockiert Inline-Event-Handler.

javascript:-URIs refaktorieren

Durch CSP zulässig
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
Die CSP lässt Event-Handler zu, die mit JavaScript registriert wurden.
Durch CSP blockiert
<a href="javascript:linkClicked()">foo</a>
CSP blockiert javascript:-URIs.

eval() aus dem JavaScript entfernen

Wenn Ihre Anwendung eval() verwendet, um JSON-String-Serialisierungen in JS-Objekte zu konvertieren, sollten Sie solche Instanzen in JSON.parse() umgestalten, was auch schneller ist.

Wenn Sie nicht alle Verwendungen von eval() entfernen können, können Sie trotzdem eine strenge Nonce-basierte CSP festlegen. Sie müssen dann aber das CSP-Schlüsselwort 'unsafe-eval' verwenden, wodurch Ihre Richtlinie etwas weniger sicher wird.

Diese und weitere Beispiele für ein solches Refactoring finden Sie in diesem Codelab zu strikten CSPs:

Schritt 4 (optional): Fallbacks hinzufügen, um alte Browserversionen zu unterstützen

Browser Support

  • Chrome: 52.
  • Edge: 79.
  • Firefox: 52.
  • Safari: 15.4.

Wenn Sie ältere Browserversionen unterstützen müssen:

  • Wenn Sie strict-dynamic verwenden, müssen Sie https: als Fallback für frühere Safari-Versionen hinzufügen. Dabei gilt:
    • Alle Browser, die strict-dynamic unterstützen, ignorieren den https:-Fallback. Dadurch wird die Stärke der Richtlinie nicht verringert.
    • In alten Browsern können extern bezogene Scripts nur geladen werden, wenn sie von einem HTTPS-Ursprung stammen. Diese Methode ist weniger sicher als eine strikte CSP, verhindert aber dennoch einige häufige XSS-Ursachen wie das Einfügen von javascript:-URIs.
  • Um die Kompatibilität mit sehr alten Browserversionen (4+ Jahre) zu gewährleisten, können Sie unsafe-inline als Fallback hinzufügen. Alle aktuellen Browser ignorieren unsafe-inline, wenn eine CSP-Nonce oder ein CSP-Hash vorhanden ist.
Content-Security-Policy:
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

Schritt 5: CSP bereitstellen

Nachdem Sie sich vergewissert haben, dass Ihre CSP keine legitimen Skripts in Ihrer lokalen Entwicklungsumgebung blockiert, können Sie Ihre CSP in der Staging-Umgebung und dann in Ihrer Produktionsumgebung bereitstellen:

  1. (Optional) Stellen Sie Ihre CSP im Modus „Nur Berichterstellung“ mit dem Header Content-Security-Policy-Report-Only bereit. Der Modus „Nur Berichterstellung“ ist nützlich, um eine potenziell funktionsgefährdende Änderung wie eine neue CSP in der Produktion zu testen, bevor Sie CSP-Einschränkungen erzwingen. Im Nur-Berichtsmodus hat Ihre CSP keine Auswirkungen auf das Verhalten Ihrer App. Der Browser generiert jedoch weiterhin Konsolenfehler und Berichte zu Verstößen, wenn er auf Muster stößt, die nicht mit Ihrer CSP kompatibel sind. So können Sie sehen, was für Ihre Endnutzer nicht funktioniert hätte. Weitere Informationen finden Sie unter Reporting API.
  2. Wenn Sie sicher sind, dass Ihre CSP die Website für Ihre Endnutzer nicht beeinträchtigt, stellen Sie sie mit dem Antwortheader Content-Security-Policy bereit. Wir empfehlen, die CSP serverseitig über einen HTTP-Header festzulegen, da dies sicherer ist als ein <meta>-Tag. Nachdem Sie diesen Schritt abgeschlossen haben, schützt Ihre CSP Ihre Anwendung vor XSS.

Beschränkungen

Eine strenge CSP bietet im Allgemeinen eine starke zusätzliche Sicherheitsebene, die zur Minimierung von XSS beiträgt. In den meisten Fällen wird die Angriffsfläche durch die CSP erheblich reduziert, da gefährliche Muster wie javascript:-URIs abgelehnt werden. Je nach Art der verwendeten CSP (Nonces, Hashes, mit oder ohne 'strict-dynamic') gibt es jedoch Fälle, in denen die CSP Ihre App nicht so gut schützt:

  • Wenn Sie ein Script mit einer Nonce versehen, aber eine direkte Injection in den Text oder den Parameter src dieses <script>-Elements erfolgt.
  • Wenn Injektionen in die Speicherorte dynamisch erstellter Skripts (document.createElement('script')) erfolgen, einschließlich in Bibliotheksfunktionen, die script-DOM-Knoten basierend auf den Werten ihrer Argumente erstellen. Dazu gehören einige gängige APIs wie .html() von jQuery sowie .get() und .post() in jQuery < 3.0.
  • Wenn es in alten AngularJS-Anwendungen Vorlagen-Injections gibt. Ein Angreifer, der in eine AngularJS-Vorlage einschleusen kann, kann damit beliebigen JavaScript-Code ausführen.
  • Wenn die Richtlinie 'unsafe-eval' enthält, sind Injektionen in eval(), setTimeout() und einige andere selten verwendete APIs zulässig.

Entwickler und Sicherheitstechniker sollten bei Code Reviews und Sicherheitsaudits besonders auf solche Muster achten. Weitere Informationen zu diesen Fällen finden Sie unter Content Security Policy: A Successful Mess Between Hardening and Mitigation.

Weitere Informationen