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

Lukas Weichselbaum
Lukas Weichselbaum

Unterstützte Browser

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

Quelle

Cross-Site-Scripting (XSS), die Möglichkeit, schädliche Scripts 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 Content-Security-Policy-HTTP-Header 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 anstelle der gängigen CSPs mit Host-Zulassungslisten, die die Seite häufig an XSS aussetzen, da sie in den meisten Konfigurationen umgangen werden können, einen CSP auf Grundlage von Nonces oder Hashes verwenden, um XSS zu minimieren.

Wichtiger Begriff: Ein Nonce ist eine zufällige Zahl, 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 strenge CSP bezeichnet. Wenn eine Anwendung ein strenges CSP verwendet, können Angreifer, die HTML-Injection-Fehler finden, den Browser in der Regel nicht dazu zwingen, schädliche Scripts in einem angreifbaren Dokument auszuführen. Das liegt daran, dass bei strikter CSP nur gehashte Scripts oder Scripts mit dem korrekten Nonce-Wert zulässig sind, der auf dem Server generiert wird. Angreifer können das Script also nicht ausführen, ohne den korrekten Nonce-Wert für eine bestimmte Antwort zu kennen.

Warum sollten Sie eine strenge CSP verwenden?

Wenn Ihre Website bereits einen CSP hat, der so aussieht wie script-src www.googleapis.com, ist er wahrscheinlich nicht effektiv gegen websiteübergreifende Angriffe. Dieser CSP-Typ wird als CSP mit Zulassungsliste bezeichnet. Sie erfordern viel Anpassung und können von Angreifern umgangen werden.

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

Strikte CSP-Struktur

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

Nonces-basierte strenge CSP

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

Hashbasierte strenge 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 „streng“ und somit sicher:

  • Dabei werden Nonces 'nonce-{RANDOM}' oder Hashes 'sha256-{HASHED_INLINE_SCRIPT}' verwendet, um anzugeben, welche <script>-Tags der Entwickler der Website für die Ausführung im Browser des Nutzers vertrauenswürdig hält.
  • Damit wird 'strict-dynamic' festgelegt, um den Aufwand für die Bereitstellung eines nonce- oder hashbasierten CSP zu reduzieren. Dadurch wird die Ausführung von Scripts, die von einem vertrauenswürdigen Script erstellt werden, automatisch zugelassen. Dadurch wird auch die Verwendung der meisten JavaScript-Bibliotheken und ‑Widgets von Drittanbietern freigeschaltet.
  • Da er nicht auf URL-Zulassungslisten basiert, ist er nicht anfällig für häufige CSP-Umgehungen.
  • Es werden nicht vertrauenswürdige Inline-Scripts wie Inline-Ereignis-Handler oder javascript:-URIs blockiert.
  • Dadurch wird object-src daran gehindert, gefährliche Plug-ins wie Flash zu deaktivieren.
  • Damit wird base-uri eingeschränkt, um die Einschleusung von <base>-Tags zu blockieren. So wird verhindert, dass Angreifer die Speicherorte von Scripts ändern, die über relative URLs geladen werden.

Strikte CSP verwenden

Wenn Sie eine strenge CSP verwenden möchten, müssen Sie Folgendes tun:

  1. Entscheiden Sie, ob für Ihre Anwendung ein nonce- oder hashbasierter CSP festgelegt werden soll.
  2. Kopieren Sie den CSP aus dem Abschnitt Strenge CSP-Struktur und legen Sie ihn als Antwortheader in Ihrer Anwendung fest.
  3. Refaktorieren Sie HTML-Vorlagen und clientseitigen Code, um Muster zu entfernen, die nicht mit CSP kompatibel sind.
  4. Stellen Sie Ihren Content-Anbieter bereit.

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

Lighthouse meldet die Warnung, dass im erzwungenen Modus kein CSP gefunden wurde.
Wenn Ihre Website keinen CSP hat, zeigt Lighthouse diese Warnung an.

Schritt 1: Entscheiden, ob eine nonce- oder hashbasierte CSP erforderlich ist

So funktionieren die beiden Arten von strengen CSPs:

Nonce-basierte CSP

Bei einer noncebasierten CSP generieren Sie eine Zufallszahl bei der Laufzeit, 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 auf Ihrer Seite einfügen oder ausführen, da er die richtige Zufallszahl für dieses Script erraten müsste. Das funktioniert nur, wenn die Nummer nicht erraten werden kann und bei jeder Antwort zur Laufzeit neu generiert wird.

Verwenden Sie eine noncebasierte 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 Hashwert jedes Inline-Script-Tags zur CSP hinzugefügt. Jedes Script hat einen anderen Hash. Ein Angreifer kann kein schädliches Script auf Ihrer Seite einfügen oder ausführen, da der Hash dieses Scripts in Ihrem CSP enthalten sein muss, 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, React oder anderen erstellt wurden und statisch ohne serverseitiges Rendering bereitgestellt werden.

Schritt 2: Einen strengen CSP festlegen und Scripts vorbereiten

Beim Festlegen eines Content-Sicherheitsanbieters haben Sie folgende Möglichkeiten:

  • Im Modus „Nur Berichte“ (Content-Security-Policy-Report-Only) oder im Erzwingungsmodus (Content-Security-Policy). Im Modus „Nur Berichte“ blockiert der CSP noch keine Ressourcen. Daher funktioniert alles auf Ihrer Website, aber Sie sehen Fehler und erhalten Berichte zu allen Elementen, die blockiert worden wären. Wenn Sie Ihren CSP lokal festlegen, spielt das keine Rolle, da in beiden Modi die Fehler in der Browserkonsole angezeigt werden. Im Erzwingungsmodus können Sie Ressourcen finden, die von Ihrem CSP-Entwurf blockiert werden, da das Blockieren einer Ressource dazu führen kann, dass Ihre Seite nicht richtig angezeigt wird. Der Modus „Nur Berichte“ ist später im Prozess am nützlichsten (siehe Schritt 5).
  • Header oder HTML-<meta>-Tag Bei der lokalen Entwicklung kann ein <meta>-Tag praktischer sein, um die CSP anzupassen und schnell zu sehen, wie sich das auf Ihre Website auswirkt. Es gilt jedoch:
    • Wenn Sie Ihren CSP später in der Produktion bereitstellen, empfehlen wir, ihn als HTTP-Header festzulegen.
    • Wenn Sie Ihren CSP im Modus „Nur Bericht“ festlegen möchten, müssen Sie ihn als Header festlegen, da CSP-Meta-Tags diesen Modus nicht unterstützen.

Option A: Nonce-basierter CSP

Lege in deiner 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

Ein Nonce ist eine Zufallszahl, die nur einmal pro Seitenaufbau verwendet wird. Ein noncebasiertes CSP kann XSS nur dann verhindern, 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 });
});

<script>-Elementen ein nonce-Attribut hinzufügen

Bei einer noncebasierten CSP muss jedes <script>-Element ein nonce-Attribut haben, das mit dem im CSP-Header angegebenen zufälligen Nonce-Wert übereinstimmt. Alle Scripts können dieselbe Nonce haben. Fügen Sie diese Attribute zuerst allen Scripts hinzu, damit sie vom Preisvergleichsportal zugelassen werden.

Option B: Hash-basierter CSP-Antwortheader

Lege in deiner 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';

Bei mehreren Inline-Scripts lautet die Syntax: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.

Quellscripts dynamisch laden

Sie können Drittanbieter-Scripts dynamisch mit einem Inline-Script laden.

Beispiel für Inline-Scripts
Vom CSP erlaubt
<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, wobei der Platzhalter {HASHED_INLINE_SCRIPT} ersetzt wird. Um die Anzahl der Hashes zu reduzieren, können Sie alle Inline-Scripts in einem einzigen Script zusammenführen. Ein praktisches Beispiel dazu finden Sie in diesem Artikel und im entsprechenden Code.
Von CSP blockiert
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
Diese Scripts werden von CSP blockiert, da sie nicht dynamisch hinzugefügt wurden und kein integrity-Attribut haben, das mit einer zulässigen Quelle übereinstimmt.

Hinweise zum Laden von Scripts

Im Beispiel für das Inline-Script wird s.async = false hinzugefügt, damit 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 Scripts geladen werden, da die Scripts dynamisch hinzugefügt werden. Der Parser wird nur angehalten, während die Scripts ausgeführt werden, genau wie bei async-Scripts. Beachten Sie bei diesem Snippet Folgendes:

  • Ein oder beide Scripts werden möglicherweise ausgeführt, bevor der Download des Dokuments abgeschlossen ist. Wenn das Dokument zum Zeitpunkt der Ausführung der Scripts fertig sein soll, warten Sie auf das Ereignis DOMContentLoaded, bevor Sie die Scripts anhängen. Wenn dadurch ein Leistungsproblem auftritt, weil die Scripts nicht früh genug heruntergeladen werden, verwenden Sie Preload-Tags weiter oben auf der Seite.
  • defer = true hat keine Auswirkungen. Wenn Sie dieses Verhalten benötigen, führen Sie das Script bei Bedarf manuell aus.

Schritt 3: HTML-Vorlagen und clientseitigen Code überarbeiten

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

Wenn Sie CSP im vorherigen Schritt aktiviert haben, sehen Sie in der Console CSP-Verstöße, sobald CSP ein inkompatibles Muster blockiert.

Berichte zu CSP-Verstößen in der Chrome-Entwicklerkonsole
Console-Fehler für blockierten Code

In den meisten Fällen ist die Lösung ganz einfach:

Inline-Ereignis-Handler umstrukturieren

Vom CSP erlaubt
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
Mit CSP sind Event-Handler zulässig, die mit JavaScript registriert sind.
Von CSP blockiert
<span onclick="doThings();">A thing.</span>
CSP blockiert Inline-Ereignis-Handler.

javascript:-URIs umstrukturieren

Vom CSP erlaubt
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
Mit CSP sind Event-Handler zulässig, die mit JavaScript registriert werden.
Von CSP blockiert
<a href="javascript:linkClicked()">foo</a>
Der CSP blockiert javascript: URIs.

eval() aus Ihrem JavaScript entfernen

Wenn in Ihrer Anwendung eval() verwendet wird, um JSON-String-Serialisierungen in JS-Objekte umzuwandeln, sollten Sie diese Instanzen zu JSON.parse() umstrukturieren, was auch schneller ist.

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

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

Schritt 4 (optional): Fallbacks zur Unterstützung alter Browserversionen hinzufügen

Unterstützte Browser

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

Quelle

Wenn du ältere Browserversionen unterstützen möchtest:

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

Schritt 5: Content-Sicherheitsanbieter bereitstellen

Nachdem Sie bestätigt haben, dass Ihr CSP keine legitimen Scripts in Ihrer lokalen Entwicklungsumgebung blockiert, können Sie Ihren CSP in der Staging-Umgebung und dann in der Produktionsumgebung bereitstellen:

  1. Optional: Sie können Ihren CSP im Modus „Nur Berichterstellung“ mit der Kopfzeile Content-Security-Policy-Report-Only bereitstellen. Der Modus „Nur melden“ eignet sich gut, um eine potenziell fehlerhafte Änderung wie einen neuen CSP in der Produktion zu testen, bevor Sie CSP-Einschränkungen erzwingen. Im Modus „Nur Berichte“ wirkt sich Ihre CSP nicht auf das Verhalten Ihrer App aus. Der Browser generiert jedoch weiterhin Konsolenerre und Verstoßberichte, 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 Ihr CSP Ihre Website für Ihre Endnutzer nicht beeinträchtigt, können Sie ihn mit dem Content-Security-Policy-Antwortheader implementieren. Wir empfehlen, Ihren CSP serverseitig mit einem HTTP-Header festzulegen, da dies sicherer ist als ein <meta>-Tag. Nach Abschluss dieses Schritts schützt Ihr CSP Ihre App vor XSS.

Beschränkungen

Eine strenge CSP bietet in der Regel eine zusätzliche Sicherheitsebene, die dazu beiträgt, XSS-Angriffe zu minimieren. In den meisten Fällen reduziert CSP die Angriffsfläche erheblich, indem gefährliche Muster wie javascript:-URIs abgelehnt werden. Je nach CSP-Typ, den Sie verwenden (Nonces, Hashes, mit oder ohne 'strict-dynamic'), gibt es jedoch Fälle, in denen CSP Ihre App nicht ausreichend schützt:

  • Wenn Sie ein Script mit einem Nonce versehen, aber eine Injection direkt in den Body oder den src-Parameter dieses <script>-Elements erfolgt.
  • Ob es Einschleusungen an den Speicherorten von dynamisch erstellten Scripts (document.createElement('script')) gibt, einschließlich aller 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 in alten AngularJS-Anwendungen Vorlagen-Injections vorhanden sind. Ein Angreifer, der in eine AngularJS-Vorlage einschleusen kann, kann damit beliebiges JavaScript ausführen.
  • Wenn die Richtlinie 'unsafe-eval' enthält, werden Injections in eval(), setTimeout() und einige andere selten verwendete APIs verhindert.

Entwickler und Sicherheitsexperten sollten bei Codeüberprüfungen und Sicherheitsaudits besonders auf solche Muster achten. Weitere Informationen zu diesen Fällen finden Sie im Artikel Content Security Policy: A Successful Mess Between Hardening and Mitigation.

Weitere Informationen