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

Lukas Weichselbaum
Lukas Weichselbaum

Unterstützte Browser

  • 52
  • 79
  • 52
  • 15,4

Quelle

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

Die Content Security Policy (CSP) ist eine zusätzliche Sicherheitsebene zur Vermeidung von XSS. Wenn du eine CSP konfigurieren möchtest, füge einer Webseite den HTTP-Header Content-Security-Policy hinzu und lege Werte fest, die steuern, welche Ressourcen der User-Agent für diese Seite laden kann.

Auf dieser Seite wird erläutert, wie eine CSP auf der Grundlage von Nonces oder Hashes zur Minderung von XSS verwendet wird, anstelle der häufig verwendeten, auf Host-Zulassungslisten basierenden CSPs, die die Seite häufig XSS ausgesetzt lassen, da sie in den meisten Konfigurationen umgangen werden können.

Schlüsselbegriff: Eine Nonce ist eine Zufallszahl, die nur einmal verwendet wird, um ein <script>-Tag als vertrauenswürdig zu kennzeichnen.

Schlüsselbegriff: 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 Hashwert (z. B. SHA-256) verwenden, um ein Inline-<script>-Tag als vertrauenswürdig zu kennzeichnen.

Eine auf Nonces oder Hashes basierende Content Security Policy wird oft als strikte CSP bezeichnet. Wenn eine Anwendung eine strikte CSP verwendet, können Angreifer, die HTML-Injection-Fehler finden, diese im Allgemeinen nicht nutzen, um den Browser zur Ausführung schädlicher Skripts in einem anfälligen Dokument zu zwingen. Dies liegt daran, dass die strikte CSP nur gehashte Skripts oder Skripts mit dem richtigen auf dem Server generierten Nonce-Wert zulässt. Daher können Angreifer das Skript nicht ausführen, ohne die richtige Nonce für eine bestimmte Antwort zu kennen.

Warum sollte ich eine strikte CSP verwenden?

Wenn Ihre Website bereits eine CSP mit dem Format script-src www.googleapis.com hat, ist sie wahrscheinlich nicht gegen websiteübergreifende Maßnahmen wirksam. Diese Art von CSP wird als CSP auf Zulassungsliste bezeichnet. Sie erfordern ein hohes Maß an Anpassung und können von Angreifern umgangen werden.

Mit strikten CSPs, die auf kryptografischen Nonces oder Hashes basieren, werden diese Schwierigkeiten vermieden.

Strenge CSP-Struktur

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

Nicht standortbasierte, strikte CSP

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic';
  object-src 'none';
  base-uri 'none';
So funktioniert eine Nonce-basierte, strikte CSP.

Hash-basierte strikte CSP

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

Die folgenden Attribute machen eine CSP wie diese „streng“ und daher 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 ansagt.
  • Sie legt 'strict-dynamic' fest, um den Aufwand für die Bereitstellung einer Nonce- oder Hash-basierten CSP zu reduzieren, da die Ausführung von Scripts, die von einem vertrauenswürdigen Script erstellt werden, automatisch zugelassen wird. Dadurch wird auch die Verwendung der meisten JavaScript-Bibliotheken und -Widgets von Drittanbietern aufgehoben.
  • Sie basiert nicht auf URL-Zulassungslisten und weist daher keine häufigen Umgehungen von CSPs auf.
  • Sie blockiert nicht vertrauenswürdige Inline-Scripts wie Inline-Event-Handler oder javascript:-URIs.
  • object-src wird eingeschränkt, um gefährliche Plug-ins wie Flash zu deaktivieren.
  • base-uri wird eingeschränkt, um das Einschleusen von <base>-Tags zu blockieren. Dadurch wird verhindert, dass Angreifer den Speicherort von Skripts ändern, die über relative URLs geladen werden.

Eine strikte CSP anwenden

Für eine strikte CSP gelten folgende Voraussetzungen:

  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 in Ihrer Anwendung fest.
  3. Refaktorieren Sie HTML-Vorlagen und clientseitigen Code, um Muster zu entfernen, die mit der CSP nicht kompatibel sind.
  4. Stellen Sie Ihre CSP bereit.

Sie können während des gesamten Prozesses eine Prüfung der Best Practices von Lighthouse (Version 7.3.0 und höher mit dem Flag --preset=experimental) verwenden. So können Sie prüfen, ob Ihre Website eine CSP hat und ob sie streng genug für XSS ist.

Warnung im Lighthouse-Bericht, dass im Erzwingungsmodus keine CSP gefunden wurde.
Wenn Ihre Website keine CSP hat, zeigt Lighthouse diese Warnung an.

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

Die zwei Arten strenger CSPs funktionieren folgendermaßen:

Nonce-basierte CSP

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

Für HTML-Seiten, die auf dem Server gerendert werden, solltest du eine Nonce-basierte CSP verwenden. 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 auf Ihrer Seite einfügen oder ausführen, da der Hash dieses Skripts in Ihrem CSP gespeichert sein muss, damit es ausgeführt werden kann.

Verwenden Sie eine hashbasierte CSP für statisch bereitgestellte HTML-Seiten oder für 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 und statisch ohne serverseitiges Rendering bereitgestellt werden.

Schritt 2: Eine strikte CSP festlegen und Skripts vorbereiten

Beim Einrichten einer CSP haben Sie mehrere Möglichkeiten:

  • Berichtsmodus (Content-Security-Policy-Report-Only) oder Erzwingungsmodus (Content-Security-Policy). Im Nur-Berichtsmodus blockiert die CSP noch keine Ressourcen, sodass auf Ihrer Website keine Fehler auftreten. Sie können jedoch Fehler sehen und Berichte für alle Probleme erhalten, die blockiert worden wären. Lokal beim Einrichten der CSP spielt das keine Rolle, da in beiden Modi die Fehler in der Browserkonsole angezeigt werden. Wenn überhaupt, kann der Erzwingungsmodus dabei helfen, Ressourcen zu finden, die Ihr CSP-Entwurf blockiert hat, da das Blockieren einer Ressource dazu führen kann, dass Ihre Seite fehlerhaft wirkt. Der Modus "Nur Bericht" wird später am nützlichsten (siehe Schritt 5).
  • Header- oder HTML-<meta>-Tag. Für die lokale Entwicklung kann ein <meta>-Tag praktischer sein, um Ihre CSP zu optimieren und schnell zu sehen, wie es sich auf Ihre Website auswirkt. Aber:
    • Wenn Sie Ihre CSP später in der Produktion bereitstellen, empfehlen wir, sie als HTTP-Header festzulegen.
    • Wenn Sie für Ihre CSP den Modus „Nur Bericht“ verwenden möchten, müssen Sie sie als Header festlegen, da CSP-Meta-Tags den Modus „Nur Bericht“ 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 die CSP generieren

Eine Nonce ist eine Zufallszahl, die nur einmal pro Seitenaufbau verwendet wird. Eine Nonce-basierte CSP kann den XSS nur dann abschwächen, wenn Angreifer den Nonce-Wert nicht erraten können. Eine CSP-Nonce muss folgende Voraussetzungen erfüllen:

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

Hier sind einige Beispiele, wie Sie eine CSP-Nonce in serverseitigen Frameworks hinzufügen können:

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 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. Der erste Schritt besteht darin, diese Attribute allen Skripts hinzuzufügen, damit die CSP sie zulassen.

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: 'sha256-{HASHED_INLINE_SCRIPT_1}' 'sha256-{HASHED_INLINE_SCRIPT_2}'.

Quelldateien dynamisch laden

Da CSP-Hashes in Browsern nur für Inline-Scripts unterstützt werden, müssen Sie alle Drittanbieterskripts mithilfe eines Inline-Skripts dynamisch laden. Hashes für Quellskripts werden in allen Browsern nicht gut unterstützt.

Beispiel für das Inline-Einfügen von Skripts
Von CSP zugelassen
<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-Skripts berechnen und dem CSP-Antwortheader hinzufügen. Ersetzen Sie dabei den Platzhalter {HASHED_INLINE_SCRIPT}. Sie können alle Inline-Skripts zu einem einzigen Skript zusammenführen, um die Anzahl der Hashes zu reduzieren. Um dies in Aktion zu sehen, können Sie sich dieses Beispiel und den zugehörigen Code ansehen.
Von CSP blockiert
<script src="https://example.org/foo.js"></script>
<script src="https://example.org/bar.js"></script>
Die CSP blockiert diese Skripts, da nur Inline-Skripts gehasht werden können.

Überlegungen beim Laden von Skripts

Im Inline-Skriptbeispiel 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 Skripts geladen werden, da die Skripts dynamisch hinzugefügt werden. Der Parser wird wie bei async-Skripts nur während der Ausführung der Skripts beendet. Beachten Sie bei diesem Snippet jedoch Folgendes:

  • Möglicherweise wird eines oder beide Skripts ausgeführt, bevor der Download des Dokuments abgeschlossen ist. Wenn das Dokument fertig sein soll, bis die Skripts ausgeführt werden, warten Sie auf das DOMContentLoaded-Ereignis, bevor Sie die Skripts anhängen. Wenn dies ein Leistungsproblem verursacht, weil die Skripts nicht früh genug heruntergeladen werden, verwenden Sie Tags vorab laden weiter oben auf der Seite.
  • defer = true macht nichts. Wenn Sie dieses Verhalten benötigen, führen Sie das Script bei Bedarf manuell aus.

Schritt 3: HTML-Vorlagen und clientseitigen Code refaktorieren

Zum Ausführen von Skripts können Inline-Event-Handler (z. B. onclick="…", onerror="…") und JavaScript-URIs (<a href="javascript:…">) verwendet werden. Dies bedeutet, dass ein Angreifer, der einen XSS-Fehler findet, diese Art von HTML-Code einschleusen und schädlichen JavaScript-Code ausführen kann. Bei einer Nonce- oder Hash-basierten CSP ist die Verwendung dieser Art von Markup verboten. Wenn Ihre Website eines dieser Muster verwendet, müssen Sie sie in sicherere Alternativen umformulieren.

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

Berichte über CSP-Verstöße in der Chrome-Entwicklerkonsole.
Konsolenfehler aufgrund von blockiertem Code

In den meisten Fällen lässt sich das Problem ganz einfach beheben:

Inline-Event-Handler refaktorieren

Von CSP zugelassen
<span id="things">A thing.</span>
<script nonce="${nonce}">
  document.getElementById('things').addEventListener('click', doThings);
</script>
Die CSP ermöglicht Event-Handler, die mit JavaScript registriert werden.
Von CSP blockiert
<span onclick="doThings();">A thing.</span>
Die CSP blockiert Inline-Event-Handler.

javascript:-URIs refaktorieren

Von CSP zugelassen
<a id="foo">foo</a>
<script nonce="${nonce}">
  document.getElementById('foo').addEventListener('click', linkClicked);
</script>
Die CSP ermöglicht Event-Handler, die mit JavaScript registriert werden.
Von CSP blockiert
<a href="javascript:linkClicked()">foo</a>
Die CSP blockiert JavaScript-URIs.

eval() aus dem JavaScript entfernen

Wenn Ihre Anwendung eval() zum Konvertieren von JSON-String-Serialisierungen in JS-Objekte verwendet, sollten Sie solche Instanzen in JSON.parse() refaktorieren, was ebenfalls schneller ist.

Wenn Sie nicht alle Verwendungen von eval() entfernen können, haben Sie trotzdem die Möglichkeit, eine strikte Nonce-basierte CSP-basierte CSP festzulegen, aber Sie müssen das CSP-Keyword 'unsafe-eval' verwenden, wodurch Ihre Richtlinie etwas weniger sicher ist.

Diese und weitere Beispiele für eine solche Refaktorierung finden Sie in diesem strengen CSP-Codelab:

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

Unterstützte Browser

  • 52
  • 79
  • 52
  • 15,4

Quelle

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

  • Wenn Sie strict-dynamic verwenden, muss https: als Fallback für frühere Versionen von Safari hinzugefügt werden. Dabei gilt:
    • Alle Browser, die strict-dynamic unterstützen, ignorieren das https:-Fallback. Dadurch wird die Stärke der Richtlinie nicht reduziert.
    • In alten Browsern können extern stammende Skripts 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 das Einschleusen von javascript:-URIs.
  • Um die Kompatibilität mit sehr alten Browserversionen (ab 4 Jahren) zu gewährleisten, können Sie unsafe-inline als Fallback hinzufügen. Alle aktuellen Browser ignorieren unsafe-inline, wenn eine CSP-Nonce oder einen 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 sichergestellt haben, dass Ihre CSP keine legitimen Skripts in Ihrer lokalen Entwicklungsumgebung blockiert, können Sie sie für das Staging und anschließend in Ihrer Produktionsumgebung bereitstellen:

  1. (Optional) Stellen Sie die CSP mit dem Header Content-Security-Policy-Report-Only im Nur-Berichtsmodus bereit. Der reine Berichtsmodus ist praktisch, um eine funktionsgefährdende Änderung wie eine neue CSP in der Produktion zu testen, bevor Sie mit dem Erzwingen von CSP-Einschränkungen beginnen. Im Nur-Bericht-Modus hat die CSP keinen Einfluss auf das Verhalten der Anwendung. Der Browser generiert aber immer noch Konsolenfehler und Verstoßberichte, wenn er Muster findet, die mit Ihrer CSP nicht kompatibel sind. So können Sie sehen, welche Probleme für Ihre Endnutzer aufgetreten wären. Weitere Informationen finden Sie unter Reporting API.
  2. Wenn Sie sich sicher sind, dass Ihre CSP Ihre Website für Endnutzer nicht beeinträchtigt, stellen Sie sie mit dem Antwortheader Content-Security-Policy bereit. Wir empfehlen, für die CSP einen serverseitigen HTTP-Header einzurichten, da er sicherer als ein <meta>-Tag ist. Nach Abschluss dieses Schritts beginnt die CSP, Ihre Anwendung vor XSS zu schützen.

Beschränkungen

Eine strenge CSP bietet in der Regel eine starke zusätzliche Sicherheitsebene, die zur Abschwächung von XSS beiträgt. In den meisten Fällen verringert die CSP die Angriffsfläche erheblich, indem 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 Anwendung nicht ebenfalls schützt:

  • Wenn Sie ein Script, aber eine Injektion direkt in den Text oder den src-Parameter des <script>-Elements einfügen,
  • Wenn es Einschleusungen in die Speicherorte dynamisch erstellter Skripts gibt (document.createElement('script')), einschließlich in Bibliotheksfunktionen, die script-DOM-Knoten anhand der Werte ihrer Argumente erstellen. Dazu gehören einige gängige APIs wie .html() von jQuery sowie .get() und .post() in jQuery < 3.0.
  • Wenn Vorlagen in alten AngularJS-Anwendungen eingeschleust werden Ein Angreifer, der Daten in eine AngularJS-Vorlage einschleusen kann, kann damit beliebigen JavaScript-Code ausführen.
  • Wenn die Richtlinie 'unsafe-eval' enthält, Einschleusungen in eval(), setTimeout() und einige andere selten verwendete APIs.

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

Weitere Informationen