XSS: ein fremdes Skript in deiner Seite
Problem
Eine Suche, die stiehlt
Der Shop hat eine Suche. Die Ergebnisseite zeigt, was der Käufer getippt hat: „Ergebnisse für: Laufschuhe“. Der Server klebt den Suchtext in HTML:
import html
def search_page(query): return "<p>Ergebnisse für: <b>" + query + "</b></p>"
print(search_page("Laufschuhe"))print(search_page("<img src=x onerror=\"steal(shopSession)\">"))<p>Ergebnisse für: <b>Laufschuhe</b></p><p>Ergebnisse für: <b><img src=x onerror="steal(shopSession)"></b></p>Die erste Zeile ist eine gewöhnliche Seite. In der zweiten lautet die Suche <img src=x onerror="steal(shopSession)">, und sie ist vollständig im HTML angekommen. Der Browser sieht einen Bild-Tag, das Bild x lädt nicht, und er führt den Code in onerror aus. Hier ist steal eine erfundene Funktion; bei einem echten Angriff schickt der Code die Cookies oder die Sitzung an den Server des Angreifers.
Der Angreifer tippt nicht in seine eigene Suche. Er schickt den Käufern einen Link zu einer Suche im Shop mit diesem Text in der Adresse. Wer ihn öffnet, führt den fremden Code im Shop aus, mit der eigenen Sitzung. Das ist XSS (cross-site scripting): ein fremdes Skript in deiner Seite.
Die Idee
HTML ist eine weitere Sprache
Das ist die Injektion aus der vorigen Lektion, in einer weiteren Sprache. In HTML beginnt < einen Tag, und " schließt einen Attributwert. Der Code hat den Text des Käufers in die Sprache des Browsers geklebt, und die Zeichen im Text wurden Teil der Seite.
Zwei Arten kommen am häufigsten vor:
- Reflektiert (reflected): Der Text kommt aus der Adresse und geht in der Antwort zurück, wie bei der Suche oben. Der Angreifer muss jemanden dazu bringen, den Link zu öffnen.
- Gespeichert (stored): Der Text ist in der Datenbank gespeichert, zum Beispiel eine Bewertung, und wird jedem gezeigt, der das Produkt öffnet. Der Angreifer schreibt einmal, und das Skript läuft bei allen.
Der Server hat nichts ausgeführt. Der Code läuft im Browser des Käufers, mit dessen Sitzung, als wäre er Teil des Shops.
Sieh es dir an
Probier es selbst aus
Tippe einen Text in die Suche des Shops, wähle einen Schutz und sieh, was passiert. Die Seite liegt in einem isolierten iframe, ohne Netzwerk, und sieht nichts von dieser Website.
Sandbox: feindliche Eingabe in einer Shopseite
Die Seite (in einem isolierten iframe, ohne Netzwerk)
Was passiert ist
Was beim Browser ankommt
Die Sandbox ist isoliert: kein Netzwerk, und sie sieht nichts von der Website. steal() ist hier der vorgetäuschte Server des Angreifers; ein echter Angriff schickt die Daten mit fetch.
Probiere die drei fertigen Eingaben ohne Schutz, dann mit Escaping, dann mit CSP. Sieh unter „Was beim Browser ankommt“ nach, wie das HTML in den drei Fällen aussieht.
Die Idee
Die Ausgabe escapen
Die Lösung ist das Escaping: Bevor du Text in HTML einsetzt, ersetzt du die fünf Zeichen, die HTML deutet, durch ihre Entitäten. Der Browser zeigt die Entität als Zeichen, liest sie aber nicht als Teil der Seite:
| Zeichen | Wird zu | Warum |
|---|---|---|
& |
& |
beginnt eine Entität; wird als Erstes ersetzt |
< |
< |
beginnt einen Tag |
> |
> |
schließt einen Tag |
" |
" |
schließt einen Attributwert |
' |
' |
schließt einen Wert in einfachen Anführungszeichen |
html.escape ersetzt dieselben fünf Zeichen (' wird zu ', mit derselben Bedeutung).import html
def search_page(query): return "<p>Ergebnisse für: <b>" + html.escape(query) + "</b></p>"
print(search_page("Laufschuhe"))print(search_page("<img src=x onerror=\"steal(shopSession)\">"))<p>Ergebnisse für: <b>Laufschuhe</b></p><p>Ergebnisse für: <b><img src=x onerror="steal(shopSession)"></b></p>Jetzt kam die Suche als Text beim Browser an. Der Käufer sieht genau das, was er getippt hat, aber es gibt keinen Tag.
Die Regel lautet: die Ausgabe escapen, beim Anzeigen, an der Stelle, an der der Text in HTML gelangt. Nicht beim Entgegennehmen: Dann weißt du nicht, wohin der Text geht (in HTML, in JSON, in eine E-Mail), und in der Datenbank steht < statt dessen, was der Käufer getippt hat.
Genauer, für Neugierige
-
In einem echten Projekt wird HTML mit Templates erzeugt, und sie escapen selbst: Jinja2 in Flask für
.html-Dateien und die Templates von Django. Der Filter|safeundMarkup(…)schalten das Escaping ab und sind die Stelle, die man im Code-Review prüft. - Das Escaping hängt von der Stelle ab. Die fünf Zeichen genügen für Text zwischen Tags und für einen Attributwert in Anführungszeichen. Ein Attribut ohne Anführungszeichen, Text innerhalb von
<script>, Stile und eine Adresse inhrefhaben andere Regeln. Am einfachsten: Setze fremden Text nur zwischen Tags und in Attribute mit Anführungszeichen. - Die Adresse in
href.javascript:steal(shopSession)enthält keines der fünf Zeichen und führt beim Klick trotzdem Code aus. Für Adressen von Nutzern braucht es eine Allow-List, wie in der Lektion „Nicht vertrauenswürdige Eingaben“: Die Adresse beginnt mithttps://oderhttp://, sonst wird kein Link daraus.
Probier es aus
Finde das Feld
Die Seite mit den Bewertungen zeigt einen Titel, einen Autor und einen Text. Welches Feld kommt roh beim Browser an?
Wähle die Zeile mit dem Fehler.
Das letzte: der Text der Bewertung. Der Titel und der Autor gehen durch html.escape, während text so eingeklebt wird, wie er ist. Das ist gespeichertes XSS: eine Bewertung, und das Skript läuft bei jedem, der das Produkt öffnet.
Probier es aus
Was wird ausgegeben
Was gibt das Escaping für Tom & Jerry <3 aus?
import html
print(html.escape("Tom & Jerry <3"))& wird zu &, und < wird zu <. Die zweite Antwort hat & vergessen. Die dritte ist ein Escaping, das & zuletzt ersetzt: Dann ersetzt es auch das & in dem <, das es gerade eingesetzt hat, und der Käufer sieht auf der Seite <3 statt <3.
Die Idee
CSP: die zweite Linie
Auf einer großen Website gibt es Hunderte Stellen, an denen Text in HTML gelangt. Es genügt, eine zu übersehen. Deshalb gibt es einen zweiten Schutz: Content-Security-Policy (CSP). Das ist ein Header in der Antwort, der dem Browser sagt, welche Skripte laufen dürfen:
Content-Security-Policy: script-src 'self'; object-src 'none'; base-uri 'none'script-src 'self' bedeutet: nur Skripte aus Dateien derselben Website. Ein Skript, das in die Seite geschrieben ist (<script>…</script> oder onerror="…"), läuft nicht, und der Browser vermerkt den Verstoß. Deshalb muss auch das eigene JavaScript der Website in Dateien liegen. Wenn du doch ein Skript in der Seite brauchst, akzeptiert CSP eines mit einer nonce: einem Zufallswert, neu in jeder Antwort, den der Angreifer nicht kennt. So funktioniert der Schutz „CSP“ in der Sandbox.
CSP ersetzt das Escaping nicht. Sie stoppt das Skript, aber nicht fremdes HTML: Ein gefälschtes Formular „Melde dich erneut an“ oder ein ausgetauschter Preis ist kein Skript. Das Escaping ist die erste Linie, CSP das Netz darunter.
Genauer, für Neugierige
- Probiere es, ohne etwas kaputt zu machen.
Content-Security-Policy-Report-Onlymeldet nur, was die Richtlinie blockieren würde. So führt man CSP in eine alte Website ein: Erst schaut man sich die Verstöße an, dann schaltet man sie ein. - Das Cookie der Sitzung. Auch wenn das Skript läuft, ist ein Cookie mit dem Flag
HttpOnlyfür JavaScript nicht sichtbar. Das kommt in der Lektion über die Sitzungen.
Übung
Die Bewertung
Löse die Aufgabe in deinem Editor.
Eine Bewertung mit Autor. Schreibe render_review(author, text). Sie gibt <article><h3>Autor</h3><p>Text</p></article> zurück, wobei sowohl der Autor als auch der Text escapet werden. Probiere eine gewöhnliche Bewertung und einen Autor, der <script>steal(shopSession)</script> heißt:
<article><h3>Anna</h3><p>Bequem <3</p></article><article><h3><script>steal(shopSession)</script></h3><p>Super!</p></article>Weitere Aufgaben
- Ein Link in der Bewertung. Füge der Bewertung ein Feld „Website des Autors“ hinzu. Zeige es nur dann als
<a href="…">, wenn die Adresse mithttps://oderhttp://beginnt; sonst zeige es als gewöhnlichen Text. Probierejavascript:steal(shopSession). - Doppeltes Escaping. Was sieht der Käufer, wenn die Bewertung einmal beim Speichern in der Datenbank und noch einmal beim Anzeigen escapet wird? Probiere
<3.
Test
Prüfe dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Parameter und return · Strings · Nicht vertrauenswürdige Eingaben · Injektionen: wenn Daten zum Befehl werden
- Aufgaben zum Thema: Sicherheit für Entwickler