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.

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 auf Nonces oder Hashes basierende Content Security Policy wird oft als strikte 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 strikte 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

Für eine einfache, strenge Content Security Policy wird einer der folgenden HTTP-Antwortheader verwendet:

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.
  • Sie legt 'strict-dynamic' fest, um den Aufwand für die Bereitstellung einer Nonce- oder Hash-basierten CSP zu reduzieren, indem die Ausführung von Skripts, die von einem vertrauenswürdigen Script erstellt werden, automatisch zugelassen wird. 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.
  • base-uri wird eingeschränkt, das Einfügen 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 Lighthouse (Version 7.3.0 und höher mit dem Flag --preset=experimental) mit den Best Practices prüfen, um zu prüfen, ob Ihre Website eine CSP hat und ob sie streng genug ist, um gegen XSS effektiv zu sein.

Lighthouse meldet die Warnung, dass im erzwungenen Modus kein CSP gefunden wurde.
Wenn Ihre Website keine CSP hat, wird diese Warnung in Lighthouse angezeigt.

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

So funktionieren die beiden Arten von strengen CSPs:

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 Skript-Tag auf Ihrer Seite. Angreifer können kein schädliches Skript in Ihre Seite einbinden oder ausführen, da er die richtige Zufallszahl für dieses Skript ermitteln 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 Single-Page-Webanwendungen verwenden, die mit Frameworks wie Angular, React oder anderen erstellt wurden und ohne serverseitiges Rendering statisch bereitgestellt werden.

Schritt 2: Einen strengen CSP festlegen und Scripts vorbereiten

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

  • Nur-Bericht-Modus (Content-Security-Policy-Report-Only) oder Erzwingungsmodus (Content-Security-Policy). Im Nur-Bericht-Modus blockiert die CSP noch keine Ressourcen, sodass nichts auf Ihrer Website unterbrochen wird. Sie können jedoch Fehler sehen und Berichte für alle Fälle abrufen, die sonst 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 Nur-Bericht-Modus wird 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. Allerdings gilt:
    • Wenn Sie die CSP später in der Produktion bereitstellen, sollten Sie sie als HTTP-Header festlegen.
    • 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.

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:

  • Einen kryptografisch starken Zufallswert (idealerweise 128 oder mehr Bit)
  • 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.

Legen Sie den folgenden Content-Security-Policy-HTTP-Antwortheader in Ihrer Anwendung 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 Beispiel dafür 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>
Die CSP blockiert diese Skripts, weil 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 Skripts geladen werden, da die Skripts 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 jedoch 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 Preloader-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-Event-Handler refaktorieren

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 werden.
Von der 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 JavaScript entfernen

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

Wenn Sie nicht alle Verwendungen von eval() entfernen können, haben Sie trotzdem die Möglichkeit, eine strikte nonce-basierte CSP festzulegen. Sie müssen dann jedoch das CSP-Schlüsselwort 'unsafe-eval' verwenden, da Ihre Richtlinie dadurch etwas weniger sicher ist.

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, gehe so vor:

  • Wenn du strict-dynamic verwenden möchtest, musst du https: als Fallback für frühere 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 sich vergewissert haben, dass Ihre CSP keine legitimen Skripts in Ihrer lokalen Entwicklungsumgebung blockiert, können Sie die CSP für das Staging und dann in Ihrer Produktionsumgebung bereitstellen:

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

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 verwendetem CSP-Typ (Nonces, Hashes, mit oder ohne 'strict-dynamic'), gibt es jedoch Fälle, in denen die CSP Ihre Anwendung nicht auch 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.
  • Vorlageneinschleusungen in alten AngularJS-Anwendungen 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 unter Content Security Policy: Ein erfolgreiches Problem zwischen Härtung und Risikominderung.

Weitere Informationen