Cross-site scripting (XSS)
Ein Cross-Site Scripting (XSS)-Angriff ist einer, bei dem ein Angreifer in der Lage ist, eine Zielwebsite dazu zu bringen, bösartigen Code auszuführen, als ob er ein Teil der Website wäre.
Überblick
Ein Webbrowser lädt Code von vielen verschiedenen Websites herunter und führt ihn auf dem Computer des Nutzers aus. Einige dieser Websites werden sehr vertrauenswürdig sein, und der Nutzer könnte sie für sensible Operationen nutzen, wie finanzielle Transaktionen oder medizinische Beratung. Bei anderen, wie einer Gelegenheits-Spielseite, könnte der Nutzer keine solche Vertrauensbeziehung haben. Die Grundlage des Sicherheitsmodells des Browsers besteht darin, dass diese Seiten voneinander getrennt bleiben sollten, damit kein Code einer Website auf Objekte oder Zugangsdaten einer anderen zugreifen kann. Dies wird die Same-Origin-Policy genannt.
Bei einem erfolgreichen XSS-Angriff ist der Angreifer in der Lage, die Same-Origin-Policy zu untergraben, indem er die Zielwebsite dazu bringt, bösartigen Code in ihrem eigenen Kontext auszuführen, als ob es sich um denselben Ursprung handeln würde. Der Code kann dann alles tun, was der eigene Code der Website tun kann, einschließlich beispielsweise:
- Zugriff auf und/oder Modifizierung des gesamten Inhalts der geladenen Seiten der Website und aller Inhalte im lokalen Speicher
- Ausführen von HTTP-Anfragen mit den Zugangsdaten des Nutzers, wodurch sie sich als Nutzer ausgeben oder auf sensible Daten zugreifen können
Alle XSS-Angriffe hängen davon ab, dass eine Website zwei Dinge tut:
- Annahme einer Eingabe, die von einem Angreifer manipuliert worden sein könnte
- Aufnahme dieser Eingabe in eine Seite ohne sie zu säubern: das heißt, ohne sicherzustellen, dass sie nicht als JavaScript ausführbar wird.
Zwei XSS-Beispiele
In diesem Abschnitt werden wir zwei Beispielseiten durchgehen, die für einen XSS-Angriff anfällig sind.
Codeinjektion im Browser
In diesem Beispiel nehmen wir an, dass die Website der Bank des Nutzers my-bank.example.com ist. Der Nutzer ist typischerweise dort angemeldet, und der Code auf der Website kann auf die Kontodetails des Nutzers zugreifen und Transaktionen durchführen. Die Website möchte eine Willkommensnachricht anzeigen, personalisiert für den aktuellen Nutzer. Sie zeigt das Willkommen in einem heading-Element an:
<h1 id="welcome"></h1>
Die Seite erwartet, den Namen des aktuellen Nutzers in einem URL-Parameter zu finden. Sie extrahiert den Parameterwert und verwendet diesen Wert, um eine personalisierte Begrüßungsnachricht zu erstellen:
const params = new URLSearchParams(window.location.search);
const user = params.get("user");
const welcome = document.querySelector("#welcome");
welcome.innerHTML = `Welcome back, ${user}!`;
Angenommen, diese Seite wird von https://my-bank.example.com/welcome bereitgestellt. Um die Schwachstelle auszunutzen, sendet ein Angreifer dem Nutzer einen Link wie diesen:
<a
href="https://my-bank.example.com/welcome?user=<img src=x onerror=alert('hello!')>">
Get a free kitten!</a
>
Wenn der Nutzer auf den Link klickt:
- Lädt der Browser die Seite.
- Die Seite extrahiert den URL-Parameter mit dem Namen
user, dessen Wert<img src=x onerror=alert("hello!")>ist. - Die Seite weist dann diesen Wert der
innerHTML-Eigenschaft deswelcome-Elements zu, wodurch ein neues<img>-Element erstellt wird, das einensrc-Attributwert vonxhat. - Da der
src-Wert einen Fehler erzeugt, wird dieonerror-Ereignisbehandlereigenschaft ausgeführt, und der Angreifer kann seinen Code auf der Seite ausführen.
In diesem Fall zeigt der Code nur eine Warnung an, aber in einer echten Banking-Website könnte der Angreifer-Code alles tun, was der eigene Frontend-Code der Bank tun könnte.
Codeinjektion im Server
In diesem Beispiel betrachten wir eine Website mit einer Suchfunktion. Das HTML für die Suchseite könnte so aussehen:
<h1>Search</h1>
<form action="/results">
<label for="mySearch">Search for an item:</label>
<input id="mySearch" type="search" name="search" />
<input type="submit" />
</form>
Wenn der Nutzer einen Suchbegriff eingibt und "Absenden" klickt, macht der Browser eine GET-Anfrage an "/results" und enthält den Suchbegriff als URL-Parameter, wie folgt:
https://example.org/results?search=bananas
Der Server möchte eine Liste der Suchergebnisse anzeigen, mit einem Titel, der angibt, wonach der Nutzer gesucht hat. Er extrahiert den Suchbegriff aus dem URL-Parameter. So könnte dies in Express aussehen:
app.get("/results", (req, res) => {
const searchQuery = req.query.search;
const results = getResults(searchQuery); // Implementation not shown
res.send(`
<h1>You searched for ${searchQuery}</h1>
<p>Here are the results: ${results}</p>`);
});
Um diese Schwachstelle auszunutzen, sendet ein Angreifer dem Nutzer einen Link wie diesen:
<a href="http://example.org/results?search=<img src=x onerror=alert('hello')">
Get a free kitten!</a
>
Wenn der Nutzer auf den Link klickt:
- Sendet der Browser eine GET-Anfrage an den Server. Der URL-Parameter der Anfrage enthält den bösartigen Code.
- Der Server extrahiert den URL-Parameterwert und bettet ihn in die Seite ein.
- Der Server gibt die Seite an den Browser zurück, der sie ausführt.
Anatomie eines XSS-Angriffs
Wie bei allen XSS-Angriffen sind diese beiden Beispiele möglich, weil die Website:
- Eingaben verwendet, die von einem Angreifer manipuliert worden sein könnten
- Die Eingabe in die Seite aufnimmt, ohne sie zu säubern.
Beide Beispiele verwenden denselben Weg für die bösartige Eingabe: den URL-Parameter. Es gibt jedoch andere Wege, die Angreifer nutzen können.
Zum Beispiel stellen Sie sich einen Blog mit Kommentaren vor. In einem solchen Fall:
- Erlaubt die Website jedem, Kommentare über ein
<form>-Element einzureichen - Speichert die Kommentare in einer Datenbank
- Nimmt die Kommentare in Seiten auf, die der Website anderen Nutzern bereitstellt.
Wenn die Kommentare nicht gereinigt werden, sind sie potenzielle Vektoren für XSS. Diese Art von Angriff wird manchmal als gespeichertes oder persistent XSS bezeichnet und ist besonders schwerwiegend, da der infizierte Inhalt allen Nutzern, die auf die Seite zugreifen, jedes Mal, wenn sie darauf zugreifen, bereitgestellt wird.
Client- und Server-XSS
Ein großer Unterschied zwischen den beiden Beispielen besteht darin, dass der bösartige Code in verschiedenen Teilen der Codebasis der Website injiziert wird, was die Architektur der jeweiligen Website widerspiegelt.
Eine Website, die client-seitiges Rendering verwendet, wie eine Single-Page-App, modifiziert Seiten im Browser mit Hilfe von Web-APIs wie document.createElement(), entweder direkt oder indirekt durch ein Framework wie React. In diesem Prozess erfolgt die XSS-Injektion. Das sehen wir im ersten Beispiel: Der bösartige Code wird im Browser injiziert, indem ein Skript in der Seite den Wert des URL-Parameters der Element.innerHTML-Eigenschaft zuweist, die ihren Wert als HTML-Code interpretiert.
Eine Website, die server-seitiges Rendering verwendet, erstellt Seiten auf dem Server und nutzt ein Framework wie Django oder Express, meist indem Werte in Seitentemplates eingefügt werden. Wenn XSS-Injektionen passieren, geschieht dies während des Template-Prozesses auf dem Server. Das sehen wir im zweiten Beispiel: Der Code wird auf dem Server injiziert, indem der Express-Code den URL-Parameterwert in das zurückgegebene Dokument einfügt. Der XSS-Angriffscode wird dann ausgeführt, wenn der Browser die Seite auswertet.
In beiden Fällen ist der allgemeine Ansatz zur Verteidigung derselbe, und wir werden dies im nächsten Abschnitt detailliert behandeln. Die spezifischen Werkzeuge und APIs, die Sie verwenden, werden jedoch unterschiedlich sein.
Verteidigungen gegen XSS
Wenn Sie externe Eingaben in die Seiten Ihrer Website aufnehmen müssen, gibt es zwei Hauptverteidigungen gegen XSS:
- Verwenden Sie Ausgabe-Codierung und Sanitierung, um zu verhindern, dass Eingaben ausführbar werden. Wenn Sie Inhalte im Browser rendern, können Sie die Trusted Types API verwenden, um sicherzustellen, dass Eingaben durch eine Sanitisierungsfunktion verarbeitet werden, bevor sie in die Seite aufgenommen werden.
- Verwenden Sie eine Content Security Policy (CSP), um dem Browser mitzuteilen, welche JavaScript- oder CSS-Ressourcen ausgeführt werden dürfen. Dies ist eine Backup-Abwehr: Wenn die erste Verteidigung versagt und ausführbare Eingaben in eine Seite gelangen, sollte eine richtig konfigurierte CSP verhindern, dass der Browser sie ausführt.
Ausgabe-Codierung
Ausgabe-Codierung ist der Prozess, bei dem Zeichen in der Eingabestring, die sie potenziell gefährlich machen, entkommen und somit als Text behandelt werden, anstatt als Teil einer Sprache wie HTML.
Dies ist die geeignete Wahl, wenn Sie die Eingaben als Text behandeln möchten, z. B. weil Ihre Website Vorlagen verwendet, die Eingaben in Inhalte interpolieren, wie in diesem Django-Template Auszug:
<p>You searched for {{ search_term }}.</p>
Die meisten modernen Template-Engines führen automatisch Ausgabe-Codierung durch. Zum Beispiel führt die Template-Engine von Django die folgenden Umwandlungen durch:
-
<wird in<umgewandelt -
>wird in>umgewandelt -
'wird in'umgewandelt -
"wird in"umgewandelt -
&wird in&umgewandelt
Das bedeutet, dass wenn Sie <img src=x onerror=alert('XSS!')> in das oben genannte Django-Template übergeben, es in <img src=x onerror=alert('XSS!')> umgewandelt wird, was dann als folgender Text angezeigt wird:
Sie haben nach <img src=x onerror=alert('XSS!')> gesucht.
Ähnlich, wenn Sie client-seitiges Rendering mit React verwenden, werden Werte, die in JSX eingebettet sind, automatisch kodiert. Betrachten Sie beispielsweise eine JSX-Komponente wie diese:
import React from "react";
export function App(props) {
return <div>Hello, {props.name}!</div>;
}
Wenn wir <img src=x onerror=alert('XSS!')> in props.name übergeben, wird es als:
Hallo, <img src=x onerror=alert('XSS!')>!
gerendert.
Einer der wichtigsten Aspekte der Verhinderung von XSS-Angriffen ist die Verwendung einer anerkannten Template-Engine, die eine robuste Ausgabe-Codierung durchführt, und das Lesen ihrer Dokumentation, um alle Warnhinweise zu verstehen, die sie bietet.
Dokumentkontexte
Selbst wenn Sie eine Template-Engine verwenden, die automatisch HTML kodiert, müssen Sie darauf achten, wo im Dokument Sie nicht vertrauenswürdige Inhalte einfügen. Nehmen Sie zum Beispiel an, Sie haben ein Django-Template wie dieses:
<div>{{ my_input }}</div>
In diesem Kontext befindet sich die Eingabe innerhalb von <div>-Tags, sodass der Browser sie als HTML auswertet. Somit müssen Sie sich gegen den Fall schützen, dass my_input HTML ist, das ausführbaren Code definiert, wie <img src=x onerror="alert('XSS')">. Die in Django eingebaute Ausgabe-Codierung verhindert diesen Angriff, indem sie Zeichen wie < und > als HTML-Entitäten < und > kodiert.
Angenommen, das Template ist so:
<div {{ my_input }}></div>
In diesem Kontext behandelt der Browser die Variable my_input als ein HTML-Attribut. Da Django Anführungszeichen (" → ", ' → ') kodiert, wird die Nutzlast onmouseover="alert('XSS')" nicht ausgeführt. Allerdings wird eine nicht markierte Nutzlast wie onmouseover=alert(1) (oder mit Backticks, onmouseover=alert(`XSS`)) ausgeführt, da Attributwerte nicht zwingend in Anführungszeichen stehen müssen und Backticks standardmäßig nicht entkommen.
Der Browser verwendet unterschiedliche Regeln, um verschiedene Teile einer Webseite zu verarbeiten — HTML-Elemente und deren Inhalte, HTML-Attribute, Inline-Stile, Inline-Skripte. Die Art der Kodierung, die vorgenommen werden muss, hängt davon ab, in welchem Kontext die Eingaben interpoliert werden.
Was in einem Kontext sicher ist, kann in einem anderen unsicher sein, und es ist notwendig, den Kontext zu verstehen, in dem Sie nicht vertrauenswürdige Inhalte einfügen, und eine entsprechende spezielle Behandlung sicherzustellen.
-
HTML-Kontexte: Eingaben, die zwischen den Tags der meisten HTML-Elemente (außer für
<style>oder<script>) eingefügt werden, werden als HTML interpretiert. Die von Template-Engines angewendete Kodierung konzentriert sich hauptsächlich auf diesen Kontext. -
HTML-Attributkontexte: Das Einfügen von Eingaben als HTML-Attributwerte ist manchmal sicher und manchmal nicht, abhängig vom Attribut. Insbesondere Attributen, die Ereignisse behandeln wie
onblur, sind unsicher, ebenso dassrc-Attribut des<iframe>-Elements.Es ist auch wichtig, Platzhalter für eingefügte Attributwerte in Anführungszeichen zu setzen, oder ein Angreifer könnte in der Lage sein, ein zusätzliches unsicheres Attribut in den bereitgestellten Wert einzufügen. Zum Beispiel zitiert dieses Template einen eingefügten Wert nicht:
django<div class={{ my_class }}>...</div>Ein Angreifer kann dies ausnutzen, um ein Ereignisbehandlungsattribut zu injizieren, indem er eine Eingabe wie
some_id onmouseover=alert(1)verwendet. Um den Angriff zu verhindern, zitieren Sie den Platzhalter:django<div class="{{ my_class }}">...</div> -
JavaScript- und CSS-Kontexte: Das Einfügen von Eingaben innerhalb von
<script>- oder<style>-Tags ist fast immer unsicher.
Sanitierung
Template-Engines erlauben es Entwicklern typischerweise, die Ausgabe-Codierung zu deaktivieren. Dies ist notwendig, wenn Entwickler nicht vertrauenswürdige Inhalte als HTML und nicht als Text einfügen möchten. Beispielsweise deaktiviert in Django der safe-Filter die Ausgabe-Codierung, und in React hat dangerouslySetInnerHTML denselben Effekt.
In diesem Fall liegt es beim Entwickler, sicherzustellen, dass der Inhalt sicher ist, indem er ihn bereinigt.
Sanitisierung ist der Prozess, bei dem unsichere Merkmale aus einer HTML-Zeichenfolge entfernt werden: beispielsweise <script>-Tags oder Inline-Ereignis-Handler. Da eine Sanitisierung, wie eine Ausgabe-Codierung, schwer richtig umzusetzen ist, empfiehlt es sich, eine angesehene Drittanbieter-Bibliothek zu verwenden. DOMPurify wird von vielen Experten, darunter OWASP, empfohlen.
Betrachten Sie zum Beispiel eine HTML-Zeichenfolge wie:
<div>
<img src="x" onerror="alert('hello!')" />
<script>
alert("hello!");
</script>
</div>
Wenn wir dies DOMPurify übergeben, wird es zurückgegeben als:
<div>
<img src="x" />
</div>
Vertrauenswürdige Typen
Eine Funktion zu haben, die einen bestimmten Eingabestring bereinigen kann, ist eine Sache, aber alle Stellen in einer Codebasis zu finden, an denen Eingabezeichenfolgen bereinigt werden müssen, kann an sich ein sehr schwieriges Problem sein.
Wenn Sie client-seitiges Rendering im Browser implementieren, gibt es eine Reihe von Web-APIs, die unsicher sind, wenn sie mit nicht bereinigten, nicht vertrauenswürdigen Inhalten aufgerufen werden.
Zum Beispiel interpretieren die folgenden APIs ihre Zeichenfolgenargumente als HTML und verwenden es, um das DOM der Seite zu aktualisieren:
Element.innerHTML(wird auch intern von React'sdangerouslySetInnerHTMLverwendet)Element.outerHTMLElement.insertAdjacentHTML()Document.write()
Andere APIs führen ihre Argumente direkt als JavaScript aus. Zum Beispiel:
Die Trusted Types API ermöglicht es einem Entwickler, sicher zu sein, dass Eingaben immer bereinigt werden, bevor sie an eine dieser APIs übergeben werden.
Der Schlüssel zur Durchsetzung der Verwendung vertrauenswürdiger Typen ist die require-trusted-types-for CSP-Direktive. Wenn diese Direktive gesetzt ist, wird das Übergeben von Zeichenfolgenargumenten an unsichere APIs eine Ausnahme werfen:
const userInput = "I might be XSS";
const element = document.querySelector("#container");
element.innerHTML = userInput; // Throws a TypeError
Stattdessen muss ein Entwickler einen vertrauenswürdigen Typ an eine dieser APIs übergeben. Ein vertrauenswürdiger Typ ist ein Objekt, das aus einer Zeichenfolge von einem TrustedTypePolicy-Objekt erstellt wird, dessen Implementierung vom Entwickler festgelegt wird. Zum Beispiel:
// Create a policy that can create TrustedHTML values
// by sanitizing the input strings with DOMPurify library.
const sanitizer = trustedTypes.createPolicy("my-policy", {
createHTML: (input) => DOMPurify.sanitize(input),
});
const userInput = "I might be XSS";
const element = document.querySelector("#container");
const trustedHTML = sanitizer.createHTML(userInput);
element.innerHTML = trustedHTML;
Hinweis: Die Trusted Types API bietet keine Bereinigungsfunktion an: Sie ist ein Framework, in dem ein Entwickler sicher sein kann, dass eine von ihm bereitgestellte Bereinigungsfunktion aufgerufen wurde. Im obigen Beispiel verwendet der Entwickler DOMPurify als Bereinigungsfunktion für HTML-Senken innerhalb des Trusted Types Frameworks.
Die Trusted Types API verfügt noch nicht über eine gute Browser-übergreifende Unterstützung, aber wenn dies der Fall ist, wird sie eine wichtige Verteidigung gegen DOM-basierte XSS-Angriffe sein.
Bereitstellung einer CSP
Ausgabe-Codierung und Sanitisierung sind darauf ausgelegt, den Eintritt bösartiger Skripte in die Seiten einer Website zu verhindern. Eine der Hauptfunktionen einer Content Security Policy besteht darin, zu verhindern, dass bösartige Skripte selbst dann ausgeführt werden, wenn sie in den Seiten einer Website enthalten sind. Das heißt, es ist eine Sicherung, falls die anderen Verteidigungen fehlschlagen.
Der empfohlene Ansatz zur Minderung von XSS mit einer CSP ist eine strikte CSP, die ein Nonce oder einen Hash verwendet, um dem Browser mitzuteilen, welche Skripte er im Dokument erwartet. Wenn ein Angreifer es schafft, bösartige <script>-Elemente einzuschleusen, werden sie nicht das richtige Nonce oder den richtigen Hash haben, und der Browser wird sie nicht ausführen. Darüber hinaus sind verschiedene häufige XSS-Vektoren vollständig untersagt: Inline-Event-Handler, javascript:-URLs und APIs wie eval(), die ihre Argumente als JavaScript ausführen.
Zusammenfassende Verteidigung-Checkliste
- Verwenden Sie beim Interpolieren von Eingaben in eine Seite, entweder im Browser oder im Server, eine Template-Engine, die Ausgabe-Codierung durchführt.
- Seien Sie sich des Kontexts bewusst, in dem Sie Eingaben interpolieren, und stellen Sie sicher, dass die entsprechende Ausgabe-Codierung in diesem Kontext durchgeführt wird.
- Wenn Sie Eingaben als HTML einfügen müssen, säubern Sie sie mit einer renommierten Bibliothek. Wenn Sie dies im Browser durchführen, verwenden Sie das Trusted Types Framework, um sicherzustellen, dass die Eingabe von Ihrer Sanitisierungsfunktion verarbeitet wird.
- Implementieren Sie eine strikte CSP.