Prüfung zum Registered Information Security Specialist – Herbst 2023 (Reiwa 5), Nachmittag, Aufgabe 1 erklärt – Das Stored XSS, bei dem von 16 Bewertungen nur 2 angezeigt werden
· Go Komura · Registered Information Security Specialist, RISS-Prüfung, XSS, Cross-Site-Scripting, Web-Anwendung, Informationssicherheit, Schwachstelle, IPA, Sitzungsverwaltung
„Es sollen 16 Bewertungen vorliegen, aber nur 2 werden angezeigt.“
Die Prüfung zum Registered Information Security Specialist, Herbst 2023 (Reiwa 5), Nachmittag, Aufgabe 1, beginnt mit genau dieser Rückmeldung eines Mitglieds1. Am Bildschirmablauf ist nichts auffällig, und es erscheint kein Fehler. Nur die angezeigte Anzahl stimmt nicht.
Die Ursache war Stored Cross-Site-Scripting (XSS). Was an dieser Aufgabe interessant ist, ist aber nicht die Antwort „XSS“ selbst. Es ist, dass jede einzelne der „plausibel wirkenden Gegenmaßnahmen“, die Unternehmen Q (der im Aufgabentext auftretende E-Commerce-Betreiber) bereits eingerichtet hatte, glatt umgangen wurde. Das 50-Zeichen-Limit für den Bewertungstitel wurde durchbrochen, das für den Upload nötige Token wurde auf völlig regulärem Weg beschafft, und die gestohlene Sitzungs-ID wurde abtransportiert, ohne je an einen externen Server gesendet zu werden.
Dieser Artikel arbeitet diese Aufgabe Teilaufgabe für Teilaufgabe durch und ordnet ein, wo die Grenze zwischen den Gegenmaßnahmen verläuft, die nicht griffen, und denen, die gegriffen hätten.
Was Sie aus diesem Artikel mitnehmen, ist neben der Prüfungslösung (der Musterlösung zu jeder Teilaufgabe samt Begründung) ein in der Praxis einsetzbares Gesamtbild der XSS-Gegenmaßnahmen. Für die Prüfungsvorbereitung ist der Artikel so geschrieben, dass Sie die Abschnitte nach Teilaufgabe lesen können; wer nur die Perspektive des Code-Reviews braucht, kann auch mit Kapitel 9 und 10 beginnen und den Sinn trotzdem verstehen.
Zur übergeordneten Frage, an welchem Maßstab man die Sicherheit einer ganzen Website prüfen sollte, siehe Was auch Website-Auftraggeber wissen sollten ── Die IPA-Anleitung „So sichern Sie Ihre Website“ als Checkliste nutzen. Dieser Artikel vertieft eine der dort genannten 11 Schwachstellenkategorien – XSS – anhand eines konkreten Beispiels.
1. Zunächst das Ergebnis
- Die Art der Schwachstelle ist Stored XSS. Die Zeichenkette des Angreifers wurde auf dem Server gespeichert und danach in das HTML ausgegeben, das an alle Personen ging, die die Seite öffneten. Dass das eingebettete Skript DOM-APIs verwendet, ist eine andere Frage als die, ob es sich um DOM Based XSS handelt
- Das Eingabezeichenlimit war als XSS-Gegenmaßnahme wirkungslos. Der Angreifer teilte die Beiträge in 15 Teile auf und ließ das dazwischenliegende HTML per JavaScript-Kommentar überspringen, sodass die Fragmente zu einem einzigen Skript zusammenwuchsen
- Auch das Upload-Token (ein CSRF-Schutz) war als XSS-Gegenmaßnahme wirkungslos. Das Angriffsskript beschafft sich das Token auf demselben Weg wie der reguläre Bildschirm. Solange es auf derselben Origin läuft, kann es alles tun, was ein regulärer Nutzer auch kann
- Die gestohlene Sitzungs-ID wurde nie nach außen gesendet. Das Skript legte den Cookie-Inhalt als Bilddatei namens „a.png“ in der eigenen Avatar-Upload-Funktion der Site ab, und der Angreifer holte sie sich einfach durch normales Ansehen. Ausgangskontrollen können das nicht erkennen
- Der Aufgabentext benennt drei Dinge, die Unternehmen Q ausdrücklich fehlten: Escaping bei der Ausgabe (die grundlegende Lösung) sowie das HttpOnly-Attribut des Cookies und die Formatprüfung der hochgeladenen Datei (beides Ausweichmaßnahmen). Jede einzelne davon hätte gereicht, um die Angriffskette zu unterbrechen. Die Formatprüfung allein reicht allerdings nicht aus, wenn der Angreifer die Methode ändert – dafür braucht es zusätzlich das Reencoding
- Im Aufgabentext kommt es nicht vor, aber auch
script-srcder CSP kann dieselbe Kette durchtrennen. Bei einer Konfiguration, die'unsafe-inline'nicht erlaubt, würde das eingebettete Inline-Skript von vornherein nicht ausgeführt
2. Zum Quellmaterial – Herkunft und Umgang in diesem Artikel
Behandelt wird die folgende Aufgabe.
Quelle: Prüfung zum Registered Information Security Specialist, Herbst 2023 (Reiwa 5), Nachmittag, Aufgabe 1
Die IPA erklärt, dass für die Nutzung ihrer veröffentlichten früheren Prüfungsaufgaben, sofern gesetzlich nichts anderes bestimmt ist, weder Genehmigung noch Nutzungsgebühr erforderlich sind. Das bedeutet jedoch nicht, dass sie auf das Urheberrecht verzichtet: Sie verlangt, die Quelle in der Form „Geschäftsjahr, Prüfungstermin, Prüfungskategorie, Zeitabschnitt, Aufgabennummer usw.“ anzugeben, und verlangt, eine Veränderung an einer Aufgabe entsprechend zu vermerken2.
Dieser Artikel gibt das im Aufgabenheft abgedruckte HTML und die dortigen Skripte nicht wortgetreu wieder. Wo es zur Erklärung des Mechanismus nötig ist, ersetzen wir sie durch von uns selbst geschriebenen, gleichwertigen Beispielcode. Auch der Wortlaut der Teilaufgaben und der Musterlösungen wird zusammengefasst wiedergegeben. Das Original-Aufgabenheft, die Musterlösungen und der Auswertungskommentar lassen sich kostenlos von der IPA-Website herunterladen; wir empfehlen, sie beim Lesen griffbereit zu haben1 3 4.
Zuordnung der Aufgaben zu diesem Artikel
Für Leserinnen und Leser, die das Aufgabenheft parallel aufgeschlagen haben, zeigt die folgende Tabelle, welche Teilaufgabe welchem Abschnitt dieses Artikels entspricht. Sie können auch direkt bei der Teilaufgabe einsteigen, die Sie lösen möchten.
| Teilaufgabe | Was gefragt wird (Zeichenlimit) | Entsprechender Abschnitt |
|---|---|---|
| Teilaufgabe 1(1) | Die Art der ausgenutzten XSS-Schwachstelle (3 Auswahlmöglichkeiten) | Kapitel 4 |
| Teilaufgabe 1(2) | Die Gegenmaßnahme bei Web-App Q (≤30 Zeichen) | Kapitel 4, „Teilaufgabe 1(2): Die Gegenmaßnahme“ |
| Teilaufgabe 2 | Die Methode, mit der ein Skript ausgeführt wurde, das länger als das Eingabezeichenlimit war (≤50 Zeichen) | Kapitel 5 |
| Teilaufgabe 3(1) | Was Zeile 6 bis 20 des Angriffsskripts bewirken (≤60 Zeichen) | Kapitel 6 |
| Teilaufgabe 3(2) | Wie der Angreifer an die hochgeladene Information gelangt (≤50 Zeichen) | Kapitel 7 |
| Teilaufgabe 3(3) | Was sich mit der erlangten Information anstellen lässt (≤40 Zeichen) | Kapitel 7, „Teilaufgabe 3(3): Was sich mit der Sitzungs-ID anstellen lässt“ |
| Teilaufgabe 4 | Der Browser-Mechanismus, wegen dem der Angriff von der Domain des Angreifers aus nicht gelingt (≤40 Zeichen) | Kapitel 8 |
Wer die Prüfungsfragen überspringen und nur die praktischen Erkenntnisse braucht, sollte mit Kapitel 9 (die Liste der Gegenmaßnahmen, die griffen, und derer, die nicht griffen) und Kapitel 10 (Prüfpunkte für das Code-Review) beginnen. Wer die gesamte Angriffskette auf einen Blick sehen möchte: Das Sequenzdiagramm in Kapitel 7 zeigt den vollständigen Ablauf vom Posten bis zur Abholung.
Entsprechung zwischen dem Aufgabenheft und den Beispielen dieses Artikels
Damit Sie mit dem Originaltext abgleichen können, fassen wir hier zusammen, was wie ersetzt wurde.
| Angabe im Aufgabenheft | Umgang in diesem Artikel | Fundstelle |
|---|---|---|
| Das HTML von Seite V (einschließlich der aufgeteilt geposteten Bewertungstitel) | Nicht wortgetreu wiedergegeben; wir haben ein gleichwertiges Beispiel geschrieben, das denselben Mechanismus auf 3 Beiträge verdichtet | Kapitel 5 |
| Das extrahierte Angriffsskript (etwa 20 Zeilen) | Nicht wortgetreu wiedergegeben; wir haben gleichwertiges JavaScript geschrieben, das dieselbe Verarbeitung durchführt. Die Kommentare im Code dienen der Erläuterung in diesem Artikel | Kapitel 6 |
| Der Wortlaut jeder Teilaufgabe | Sinngemäß zusammengefasst (Bedingungen wie das Zeichenlimit entsprechen dem Original) | Jeweils zu Beginn der Kapitel 4 bis 8 |
| Musterlösungen | Von der IPA veröffentlicht3 | Verstreut in den Kapiteln 4 bis 8 |
| Auswertungskommentar | Die jeweils passende Stelle aus dem von der IPA veröffentlichten Auswertungskommentar4 | Kapitel 4, 5, 7 |
| Die Spezifikation im Aufgabentext (Unternehmen Q, Seite V, Mitglieder A/B, Funktionen und Zeichenlimits usw.) | Entsprechend dem Originaltext zusammengefasst | Kapitel 2, „Der Schauplatz“ |
Der Schauplatz
Es geht um Unternehmen Q, einen E-Commerce-Betreiber für Bekleidung mit 100 Mitarbeitenden. Es betreibt seine E-Commerce-Site mit dem selbst entwickelten System „Web-App Q“, auf das Nutzerinnen und Nutzer per HTTPS zugreifen. Die Ausgangslage in dieser Aufgabe ist, dass Unternehmen Q gerade eine Produktbewertungsfunktion für Mitglieder eingeführt hat.
Fünf Spezifikationspunkte sind dabei wichtig.
| Funktion | Spezifikation |
|---|---|
| Login | Authentifiziert mit Mitglieds-ID und Passwort und gibt eine Sitzungs-ID als Cookie aus |
| Produktbewertung | Nur eingeloggte Mitglieder können posten. Der Bewertungstitel hat ein Limit von 50 Zeichen, der Bewertungstext von 300 Zeichen, beide als freies Textfeld |
| Mitgliedsprofil | Bietet eine Seite zum Hochladen eines Avatar-Bilds und eine Seite zur Registrierung von Kreditkarteninformationen. Beide sind nur eingeloggten Mitgliedern zugänglich |
| Upload des Avatar-Bilds | Sendet eine Bilddatei und ein Token als Parameter an /user/upload. Gelingt nur, wenn das Token mit dem unter /user/profile ausgegebenen übereinstimmt |
| Anzeige des Avatar-Bilds | Ein hochgeladenes Avatar-Bild wird auf der Profileinstellungsseite des Mitglieds sowie auf Bewertungsseiten angezeigt |
Die letzten beiden Zeilen werden später wichtig.
3. Das Symptom – Warum aus 16 nur 2 wurden
Ein Mitglied meldet: „Auf der Bewertungsseite für ein einfarbiges T-Shirt (Seite V) sollten 16 Bewertungen stehen, aber nur 2 werden angezeigt.“ Als N aus der Entwicklungsabteilung Seite V öffnet, zeigt der Bildschirm Folgendes:
- Die Kopfzeile zeigt „16 Bewertungen“
- Eine Bewertung von Mitglied A (Titel „Good“, Text „Nice shirt!“)
- Eine Bewertung von Mitglied B
- Am Ende steht „Das sind alle 16 Bewertungen“
Die angezeigte Anzahl lautet 16, tatsächlich aufgelistet sind aber nur 2. Prüft man an dieser Stelle das HTML, zeigt sich, dass 15 Beiträge von Mitglied A vorliegen, in die ein langes Skript eingebettet ist.
Mit anderen Worten: Der Inhalt aller 16 Bewertungen wird tatsächlich ins HTML ausgegeben. Dass nur 2 sichtbar sind, liegt daran, dass der größte Teil davon innerhalb eines <script>-Elements landet, sodass der Browser aufhört, ihn als anzuzeigenden Inhalt zu behandeln.
Genau genommen ist der verschluckte Bereich nicht „alle 15 Beiträge“. Das öffnende <script>-Tag erscheint mitten im Titel des ersten Beitrags (unmittelbar nach „Good“), und das schließende </script>-Tag erscheint am Ende des Titels des 15. Beitrags. Daraus folgt:
- Die erste Karte (Avatar, Anzeigename, Datum, Sterne-Bewertung und der Titel bis „Good“) liegt vor
<script>und wird angezeigt - Alles von dort bis zum Ende des 15. Titels wird als Inhalt des script-Elements verschluckt
- Der Rest des 15. Beitrags (der Text „Nice shirt!“) liegt nach
</script>und wird angezeigt
Im Ergebnis fügt sich der Kopfbereich des ersten Beitrags mit dem Text des 15. zusammen, sodass es auf dem Bildschirm wie „eine einzelne Bewertung von Mitglied A mit dem Titel Good und dem Text Nice shirt!“ aussieht. Zählt man die eine Bewertung von Mitglied B hinzu, ergeben sich 2. Dass der Angreifer am Anfang des ersten Beitrags das natürlich klingende „Good“ und im Text des 15. Beitrags „Nice shirt!“ platziert hat, diente vermutlich dazu, dass die zerstörte Darstellung nicht unnatürlich auffällt.
Diese Art, ein Symptom zu lesen – „die Anzahl stimmt, die Anzeige aber nicht“ – ist auch in der Praxis nützlich. Sie ist ein Einstiegspunkt, um zu vermuten, dass die Struktur des ausgegebenen HTML zerstört wurde, statt einen Fehler in der Logik der Anzeigeanzahl zu suchen.
4. Warum „Stored XSS“ – Teilaufgabe 1
Teilaufgabe 1 lässt Sie die Art der in diesem Angriff genutzten XSS-Schwachstelle aus drei Möglichkeiten wählen: DOM Based XSS, Stored XSS oder Reflected XSS. Die richtige Antwort ist Stored XSS.
Und IPAs Auswertungskommentar zu dieser Teilaufgabe schreibt Folgendes.
Die Erfolgsquote war durchschnittlich, aber es fielen einige Prüflinge auf, die – wohl weil das Skript das DOM verwendete – fälschlicherweise mit „DOM Based XSS“ antworteten.
Das im Aufgabentext auftretende Angriffsskript verwendet XMLHttpRequest, empfängt die Antwort als DOM und greift mit getElementById auf ein Element zu. Es fasst zweifellos das DOM an. Das hat aber nichts mit der Art der Schwachstelle zu tun.
Der entscheidende Punkt: Wo wird die Angriffszeichenkette zum Skript
Was die drei Kategorien voneinander trennt, ist, wo die Angriffszeichenkette zu ausführbarem Code wird – also ob sich die verwundbare Ausgabestelle (die Senke) auf der Serverseite oder auf der Browserseite befindet.
| Art | Senke (wo die Angriffszeichenkette ausführbar wird) | Woher die Angriffszeichenkette stammt |
|---|---|---|
| Reflected XSS | Die HTML-Zusammenstellung des Servers | Die Anfrage selbst – etwa ein URL-Parameter, den der Angreifer präpariert hat |
| Stored XSS | Die HTML-Zusammenstellung des Servers | Auf der Serverseite gespeicherte Daten |
| DOM Based XSS | JavaScript im Browser (Zuweisung an innerHTML, eval, document.write usw.) |
Ein URL-Fragment, postMessage, ein vom Server empfangener Wert usw. |
In dieser Aufgabe hat der Server den vom Angreifer geposteten Bewertungstitel unverändert in das zurückgegebene HTML eingebettet. Die Senke liegt in der serverseitigen Ausgabeverarbeitung, und die Zeichenkette wird in der Datenbank gespeichert und danach an alle verteilt, die anschließend Seite V öffnen. Deshalb handelt es sich um Stored XSS.
flowchart TD
A["Die Zeichenkette des Angreifers<br/>wurde als Skript ausgeführt"] --> B{"Wo wurde sie<br/>ausführbar?"}
B -->|"Browserseitiges JS hat sie<br/>an innerHTML o. ä.<br/>übergeben"| C["DOM Based XSS"]
B -->|"Sie steckte im HTML,<br/>das der Server<br/>zusammengestellt hat"| D{"Ist diese Zeichenkette<br/>auf dem Server<br/>gespeichert?"}
D -->|"Gespeichert –<br/>ab jetzt an alle ausgegeben"| E["Stored XSS"]
D -->|"Nicht gespeichert –<br/>nur für diese eine Anfrage"| F["Reflected XSS"]
Es ist riskant, das so zu vereinfachen: „Steht die Angriffszeichenkette in der Antwort des Servers, ist es Stored oder Reflected XSS.“ Selbst wenn der Server den Wert als harmlosen Text oder als JSON zurückgibt, wird daraus DOM Based XSS, sobald browserseitiges JavaScript ihn innerHTML zuweist (das sogenannte stored DOM XSS, bei dem ein gespeicherter Wert die Ursache ist). In diesem Fall muss nicht die Ausgabeverarbeitung des Servers, sondern die clientseitige Senke korrigiert werden – entscheiden Sie also nicht danach, ob die Zeichenkette in der Antwort sichtbar ist, sondern danach, wo sie ausführbar wurde.
Was das eingebettete Skript tut (DOM anfassen, kommunizieren, Cookies lesen) und wie dieses Skript in die Seite gelangt ist, müssen getrennt betrachtet werden. Diese beiden zu verwechseln ist heikel – nicht, weil es die Wahl der Gegenmaßnahme erschwert, sondern weil es zur falschen Gegenmaßnahme führt. Urteilt man auf DOM Based XSS, kommt man zu dem Schluss „es reicht, das clientseitige JavaScript zu korrigieren“ – tatsächlich musste aber die serverseitige Ausgabeverarbeitung korrigiert werden.
Teilaufgabe 1(2): Die Gegenmaßnahme
Teilaufgabe 1(2) verlangt in ≤30 Zeichen die Gegenmaßnahme bei Web-App Q. Die Musterlösung lautet: „Escaping auf den Bewertungstitel anwenden, bevor er ausgegeben wird.“
Wichtig ist hier, dass die Reihenfolge „bevor er ausgegeben wird“ ausdrücklich genannt wird. Man escaped nicht bei Erhalt der Eingabe, sondern unmittelbar vor der Ausgabe als HTML, passend zum Kontext des Ziels. Auch IPAs „So sichern Sie Ihre Website“ nennt als erste grundlegende Lösung gegen Cross-Site-Scripting, „auf alle auf der Webseite ausgegebenen Elemente ein Escaping anzuwenden“5.
„Ist Escaping bei der Eingabe nicht wichtig für SQL-Injection?“
An dieser Stelle kommt immer diese Frage auf. Kurz gesagt: Auch bei SQL-Injection ist „Escaping bei der Eingabe“ keine Gegenmaßnahme.
Was die IPA als grundlegende Lösung gegen SQL-Injection nennt, ist, „den Aufbau von SQL-Anweisungen ausnahmslos mit Platzhaltern zu implementieren“. Für den Fall, dass SQL-Anweisungen zwangsläufig durch Zeichenkettenverkettung aufgebaut werden, wird Escaping als Alternative genannt, aber auch dort steht geschrieben: „Erfolgt der Aufbau der SQL-Anweisung durch Zeichenkettenverkettung, ist eine API der Datenbank-Engine zu verwenden, die ein Escaping durchführt, um die Literale der SQL-Anweisung korrekt zu bilden“ – das Escaping geschieht also im Moment des Aufbaus der SQL-Anweisung6, nicht zum Zeitpunkt des Empfangs der Eingabe.
Das ist also genau dieselbe Struktur wie bei HTML. Das gemeinsame Prinzip lautet:
Escaping geschieht dort, wo entschieden wird, in welche Grammatik die Daten hinausgehen.
- Geht es als HTML hinaus: HTML-Escaping
- Wird es Teil einer SQL-Anweisung: Platzhalter (notfalls Escaping als SQL-Literal)
- Wird es Teil eines Shell-Befehls: Verarbeitung nach den Regeln der Shell
Warum nicht bei der Eingabe? Weil zum Zeitpunkt des Empfangs der Eingabe noch nicht feststeht, wohin diese Daten künftig gehen. Derselbe Bewertungstext geht als HTML-Seite hinaus, als CSV-Export, als JSON-API-Antwort, als Benachrichtigungs-E-Mail, als Log-Eintrag. Wendet man bei der Eingabe HTML-Escaping an, erscheint in der CSV die Zeichenkette & unverändert, und der Datenbankinhalt weicht von der ursprünglichen Eingabe ab, sodass Suche und Auswertung durcheinandergeraten. Es begünstigt zudem doppeltes Escaping.
Muss die Eingabeseite also gar nichts tun?
Nein. Was die Eingabeseite tun sollte, ist aber nicht Escaping, sondern Validierung. Die Rollen sind verschieden.
- Bei der Eingabe „zurückweisen“ – Werte ablehnen, die nach der Spezifikation gar nicht vorkommen dürfen. Ein Postleitzahlenfeld weist alles außer 7 Ziffern zurück, ein Mengenfeld weist negative Zahlen zurück. Das ist ein eigenständig notwendiger Vorgang zum Schutz der Korrektheit der Daten
- Bei der Ausgabe „escapen“ – die akzeptierten Daten sicher in der Grammatik des jeweiligen Ziels darstellen. Das ist die grundlegende Lösung der Schwachstelle
Und wichtig ist auch, von der eingabeseitigen Validierung nicht zu viel als XSS-Gegenmaßnahme zu erwarten. Die IPA erwähnt die Methode, zu prüfen, ob Eingabewerte der Spezifikation der Anwendung entsprechen, und stellt dann ausdrücklich fest, dass die Wirksamkeit dieser Gegenmaßnahme begrenzt ist und sie keine Gegenmaßnahme darstellt, wenn die geforderte Spezifikation der Anwendung Eingaben mit einer breiten Palette an Zeichenarten erlaubt – man sich auf diese Methode daher nicht verlassen sollte5.
Der Bewertungstitel und der Bewertungstext in dieser Aufgabe waren genau das: frei formulierbare Felder, deren Spezifikation eine breite Palette an Zeichenarten erlaubt. Es gibt zwar Obergrenzen von 50 beziehungsweise 300 Zeichen, aber die Anzahl der Zeichen, die nötig ist, um ein Skript auszuführen, liegt weit darunter – eine Obergrenze allein bietet also keinen Schutz. Was bei Unternehmen Q tatsächlich geschah, sehen wir bei der nächsten Teilaufgabe 2.
5. Wie die 50-Zeichen-Grenze durchbrochen wurde – Teilaufgabe 2
Teilaufgabe 2 ist der Glanzpunkt dieser Aufgabe.
Beschreiben Sie zu Abbildung 3 in ≤50 Zeichen die Methode, mit der ein Skript ausgeführt wurde, das länger als das Eingabezeichenlimit war.
Die Musterlösung lautet: „Das HTML wurde durch mehrfach aufgeteilte Beiträge auskommentiert, sodass daraus ein einziges Skript wurde.“
Der Angreifer hat die Nutzlast in Längen aufgeteilt, die jeweils in einen einzelnen Beitrag passen, und 15-mal gepostet. Der springende Punkt ist, wie mit dem HTML umzugehen ist, das zwangsläufig zwischen den Beiträgen steht (</div>, <div class="..."> und Ähnliches) – als JavaScript wäre das ein Syntaxfehler.
Dafür wurde der JavaScript-Blockkommentar genutzt. Das Folgende ist eine vereinfachte Version, die wir zur Erklärung des Mechanismus selbst geschrieben haben (keine direkte Wiedergabe der Abbildung aus dem Aufgabenheft).
Angenommen, Folgendes wird auf 3 Beiträge verteilt in das Feld für den Bewertungstitel gepostet.
Beitrag 1: Wunderbar<script>a=1;/*
Beitrag 2: */b=2;/*
Beitrag 3: */c=3;</script>
Der Server gibt jeden davon als Bewertungstitel in HTML wie folgt aus.
<div class="review-title">Wunderbar<script>a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;</script></div>
Aus Sicht des Browsers ist alles vom ersten <script> bis zum letzten </script> ein einziges script-Element. Sein Inhalt sieht so aus:
a=1;/*</div>
<div class="description">…</div>
<div class="review-title">*/b=2;/*</div>
<div class="description">…</div>
<div class="review-title">*/c=3;
Das HTML zwischen /* und */ wird als JavaScript-Kommentar überlesen, sodass tatsächlich nur a=1; b=2; c=3; ausgeführt wird. Und weil der als Kommentar überlesene Teil trotzdem Inhalt eines script-Elements ist, wird er auf dem Bildschirm nicht angezeigt. Das ist die wahre Ursache des Symptoms „16 sieht aus wie 2“.
Was Sie daraus mitnehmen sollten
Verwechseln Sie hier Ursache und Wirkung nicht. Das Eingabezeichenlimit versagt nicht als XSS-Gegenmaßnahme, weil die Anzahl der Beiträge unbegrenzt war. Es versagt, weil schon mit 50 Zeichen ein Skript ausgeführt werden kann. Ein einzelnes zusätzliches Event-Handler-Attribut oder ein einzelnes Tag, das ein externes Skript lädt, passt jeweils in wenige Dutzend Zeichen. Selbst wenn das Posten auf einen einzigen Beitrag begrenzt gewesen wäre, hätte das Zeichenlimit keinen Schutz geboten.
Wofür war die aufgeteilte Posting-Methode dann eigentlich da? Sie war ein Mittel, um ein rund 20 Zeilen langes Skript vollständig einzuschleusen. Was der Angreifer vorhatte, war lang, also hat er das über die Anzahl der Beiträge ausgeglichen – das ist eine andere Frage als die, warum das Zeichenlimit selbst durchbrochen wurde. Die Beschreibung der Methode und ihre Bewertung als Gegenmaßnahme müssen getrennt betrachtet werden.
Dasselbe gilt allgemein für „Beschränkungen auf der Eingabeseite“.
- Zeichenlimits, Beschränkungen der erlaubten Zeichenarten, Frontend-Validierung – diese sind aus Spezifikationsgründen nötig, ersetzen aber nicht die grundlegende Lösung gegen XSS
- Ein Angreifer hat immer Spielraum, Beschränkungen der Eingabeseite mit Mitteln wie Aufteilung, Kodierung oder Einspeisung über einen anderen Weg zu umgehen
- Zu schützen ist die Stelle, an der Daten als HTML hinausgehen
IPAs Auswertungskommentar merkt zu dieser Teilaufgabe zudem an, dass „vereinzelt Antworten zu sehen waren, die auf eine unzureichende Prüfung schließen ließen, etwa ‚die Eingabebeschränkung wurde vor dem Posten über die Entwicklertools entfernt‘“. Zwar lassen sich Frontend-Beschränkungen tatsächlich über die Entwicklertools aufheben. Was diese Teilaufgabe aber verlangte, war, aus den im HTML hinterlassenen Spuren zu erkennen, was tatsächlich geschehen ist. Das HTML in Abbildung 3 zeigt 15 Beiträge, jeweils innerhalb des Limits, aneinandergereiht, jeweils mit Kommentarzeichen an Anfang und Ende. Das ist keine Spur eines „entfernten“ Limits, sondern eine Spur eines „umgangenen“ Limits. Man sollte das so lesen, dass hier die Haltung geprüft wurde, das vom Angreifer Hinterlassene Stück für Stück zu prüfen.
Wie weit und auf welche Weise Eingabewerte validiert werden sollten, wird auch in Verwenden Sie den ausgelesenen Wert eines QR-Codes nie ungeprüft — Erfolgreiche Fehlerkorrektur garantiert nicht den Wert als Entwurf behandelt, der von außen kommenden Daten grundsätzlich misstraut.
6. Was das Skript tat – Teilaufgabe 3(1)
Das von N extrahierte Skript ist rund 20 Zeilen lang. Teilaufgabe 3(1) verlangt in ≤60 Zeichen, was Zeile 6 bis 20 davon – der Teil, der ausgeführt wird, nachdem die erste Kommunikation erfolgreich war – bewirken.
Die Musterlösung lautet: „Lädt die Sitzungs-ID zusammen mit dem aus der XHR-Antwort erhaltenen Token als Avatar-Bild hoch.“
Im Folgenden ein gleichwertiger Code, den wir selbst geschrieben haben, um dieses Verhalten zu erklären (keine direkte Wiedergabe der Abbildung aus dem Aufgabenheft).
// ① Zunächst die Profileinstellungsseite abrufen
const xhr = new XMLHttpRequest();
xhr.open("get", "https://example.jp/user/profile");
xhr.responseType = "document"; // Antwort nicht als Text, sondern als DOM empfangen
xhr.send();
xhr.onload = function () {
// ② Das Upload-Token auf demselben Weg lesen wie der reguläre Bildschirm
const token = xhr.response.getElementById("token").value;
// ③ Das Cookie (enthält die Sitzungs-ID) unverändert zum Inhalt einer PNG-Datei machen
const file = new File([document.cookie], "a.png", { type: "image/png" });
// ④ An die eigene Avatar-Upload-Funktion der Site senden
const form = new FormData();
form.append("uploadfile", file);
form.append("token", token);
const xhr2 = new XMLHttpRequest();
xhr2.open("post", "https://example.jp/user/upload");
xhr2.send(form);
};
Die ersten fünf Zeilen dienen nur dazu, Unternehmen Qs CSRF-Schutz zu umgehen
Bemerkenswert an diesem Skript ist, dass die gesamte erste Hälfte allein der Beschaffung des Tokens dient.
Unternehmen Q verlangte beim Upload des Avatar-Bilds, dass der Wert „mit dem unter /user/profile ausgegebenen Token übereinstimmt“. Das ist ein Mechanismus, der verhindern soll, dass ein auf einer anderen Site platziertes gefälschtes Formular unautorisiert einen Upload auslöst – also ein CSRF-Schutz.
Das Angriffsskript läuft aber im Browser des Opfers, auf derselben Origin wie das Opfer. Es kann also einfach genau das tun, was der reguläre Bildschirm auch tut: die Profileinstellungsseite per GET abrufen und den Wert des darin enthaltenen Elements token lesen. Allein das genügt, um an das Token zu gelangen.
Ein CSRF-Token verhindert kein XSS. Sobald XSS gelingt, kann sich der Code des Angreifers als „eingeloggtes, reguläres Mitglied“ verhalten. Was das Token schützt, ist „eine ohne Berechtigung von einer anderen Origin gesendete Anfrage“ – nicht „ein bösartiges Skript, das auf derselben Origin läuft“.
Diese Unterscheidung wirkt sich auch beim Lesen von Sicherheitsanforderungen in der Praxis aus. Die Aussage „wir haben ein CSRF-Token, also sind wir sicher“ mag zu CSRF stimmen, sagt aber nichts über XSS aus.
Was das File-Objekt bedeutet
Auch Zeile ③ verdient einen genaueren Blick. Sie erzeugt ein Dateiobjekt, dessen Inhalt die Zeichenkette aus document.cookie ist, dessen Dateiname a.png lautet und dessen MIME-Typ image/png ist.
Der Inhalt ist reiner Text. Es ist keine PNG-Datei. Der Upload gelingt trotzdem, weil, wie im Aufgabentext angegeben, Web-App Q das Format der hochgeladenen Bilddatei nicht geprüft hat.
Und dass document.cookie überhaupt lesbar war, lag daran, dass auf dem Cookie kein HttpOnly-Attribut gesetzt war. RFC 6265 definiert das HttpOnly-Attribut so: Es „beschränkt den Geltungsbereich des Cookies auf HTTP-Anfragen. Insbesondere weist es den User-Agent an, das Cookie auszulassen, wenn Zugriff auf Cookies über ‚Nicht-HTTP‘-APIs gewährt wird, etwa eine Web-Browser-API, die Cookies gegenüber Skripten offenlegt“7. Wäre dieses Attribut gesetzt gewesen, wäre die Sitzungs-ID aus der Zeichenkette herausgefallen, die Zeile ③ greift, und der Abtransport wäre wertlos gewesen.
Zu beachten ist hier, dass HttpOnly nur die Cookies verbirgt, die dieses Attribut tragen. Gibt es auf derselben Origin Cookies ohne HttpOnly (etwa für Anzeigeeinstellungen oder Tracking-IDs), liefert document.cookie diese weiterhin. „HttpOnly setzen“ bedeutet nicht, dass document.cookie leer wird. Da die Sitzungs-ID geschützt werden soll, prüfen Sie einzeln, ob genau das Sitzungs-ID-Cookie es tatsächlich gesetzt hat.
7. Datenabfluss, der die Site nie verlässt – Teilaufgaben 3(2) und 3(3)
Teilaufgabe 3(2) fragt in ≤50 Zeichen: „Wie kann der Angreifer die hochgeladene Information erlangen?“ Die Musterlösung lautet: „Lädt das Avatar-Bild des Mitglieds herunter und liest daraus die Zeichenkette der Sitzungs-ID aus.“
Erinnern Sie sich, was die Spezifikation im Aufgabentext festlegte:
Ein hochgeladenes Avatar-Bild wird auf der Profileinstellungsseite des Mitglieds sowie auf Bewertungsseiten angezeigt.
Mit anderen Worten: Das „Avatar-Bild“, in das die Sitzungs-ID des Opfers geschrieben ist, landet an einer Stelle, die auf der Site einsehbar ist. Der Angreifer muss nichts Besonderes tun. Er öffnet lediglich eine Seite, auf der das Avatar-Bild des Opfers angezeigt wird, und holt sich die URL dieses Bildes. Da der Inhalt Text ist, lässt sich die Sitzungs-ID beim Öffnen direkt lesen.
Ergänzend: Diese Abholung gelingt unmittelbar dann, wenn das Avatar-Bild des Opfers auf einer für den Angreifer einsehbaren Seite erscheint (etwa der Bewertungsseite eines Produkts, das der Angreifer selbst ebenfalls bewertet hat). Da die URL des Avatars pro Mitglied feststeht, kann der Angreifer, sobald er sie einmal irgendwo gesehen hat, sie danach direkt ansteuern. So oder so besteht die Handlung des Angreifers nur darin, als regulärer Betrachter eine Seite oder ein Bild per GET abzurufen – aus Sicht der Site sieht das nach nichts Ungewöhnlichem aus.
sequenceDiagram
autonumber
participant AT as Angreifer
participant Q as Web-App Q<br/>(example.jp)
participant V as Browser des Opfers
Note over AT,Q: Vorbereitungsphase
AT->>Q: Bewertung 15-mal aufgeteilt posten<br/>(HTML per Kommentarzeichen überspringen)
Note over Q: Geposteten Zeichenketten<br/>unverändert speichern<br/>(dieser Schritt allein ist korrekt)
Note over V,Q: Opfer öffnet Seite V
V->>Q: GET Seite V
Note over Q: ★Hier liegt die Schwachstelle★<br/>Bettet den gespeicherten Titel<br/>ohne Escaping ins HTML ein
Q-->>V: HTML mit dem Angriffsskript
Note over V: Wird als script-Element ausgeführt
V->>Q: GET /user/profile
Q-->>V: HTML mit dem Token
Note over V: Liest document.cookie<br/>und macht es zum Inhalt von a.png
V->>Q: POST /user/upload<br/>(uploadfile=a.png, token=…)
Note over Q: Speichert ohne Formatprüfung<br/>und veröffentlicht es als Avatar-Bild
Note over AT,Q: Abholung
AT->>Q: GET Avatar-Bild des Opfers
Q-->>AT: Datei mit der geschriebenen Sitzungs-ID
Ausgangskontrollen greifen hier nicht
Wie unangenehm dieser Weg ist, wird beim Diagramm deutlich: Der Browser des Opfers kommuniziert von Anfang bis Ende ausschließlich mit der legitimen Site (example.jp).
Dadurch laufen alle Gegenmaßnahmen, die den Datenverkehr beobachten, ins Leere. Nur die letzte Zeile der folgenden Tabelle wirkt tatsächlich.
| Gegenmaßnahme | Gegen diesen Angriff | Grund |
|---|---|---|
| URL-Filterung am Proxy | Wirkungslos | Das einzige Ziel ist eine geschäftlich legitime E-Commerce-Site |
| Überwachung des ausgehenden Datenverkehrs an der Firewall | Wirkungslos | Es entsteht keine Kommunikation zu einer unbekannten Domain |
connect-src-Beschränkung der Content Security Policy (eine Direktive wie 'self', die dieselbe Origin erlaubt) |
Wirkungslos | Beide Kommunikationsvorgänge zielen auf dieselbe Origin und bleiben innerhalb dessen, was die Policy erlaubt |
script-src-Beschränkung der Content Security Policy (eine Konfiguration, die 'unsafe-inline' nicht erlaubt) |
Wirksam | Das eingebettete Inline-Skript wird von vornherein nicht ausgeführt |
Bei einem Datenabfluss über XSS denkt man leicht an das Bild „das Cookie wird an den Server des Angreifers gesendet“, doch diese Aufgabe zeigt deutlich, dass das Ziel des Abflusses die Opfer-Site selbst sein kann. Jede Stelle innerhalb der Site, die zugleich beschreib- und lesbar ist, kann als Übergabepunkt dienen – eine Upload-Funktion für Dateien, ein öffentliches Profilfeld oder ein öffentliches Notizfeld sind typische Beispiele.
Zur CSP noch eine Ergänzung: Unter den in der Praxis üblichen Direktiven stoppt nicht connect-src (das Kommunikationsziele einschränkt) zuverlässig diesen Angriff, sondern script-src. Ist script-src 'self' gesetzt und 'unsafe-inline' nicht erlaubt, wird das in die Bewertung eingebettete Inline-<script> von vornherein nicht ausgeführt. Wer CSP nur als „etwas, das ausgehende Kommunikation einschränkt“ versteht, übersieht diesen Unterschied.
connect-src ist dabei nicht prinzipiell wirkungslos. Eine Direktive wie connect-src 'none', die selbst Kommunikation innerhalb derselben Origin verbietet, oder eine Allow-Liste, die auf bestimmte Endpunkte beschränkt ist, würde auch diese beiden XHR-Aufrufe stoppen. So streng lässt sich aber nur bei wenigen Sites konfigurieren, weshalb script-src als praktikable Verteidigung die erste Wahl bleibt. Die zutreffende Formulierung lautet nicht „derselbe Origin liegt außerhalb der Reichweite von CSP“, sondern „eine übliche Direktive, die dieselbe Origin erlaubt, lässt das hier durch“.
Teilaufgabe 3(3): Was sich mit der Sitzungs-ID anstellen lässt
Teilaufgabe 3(3) fragt in ≤40 Zeichen, was sich mit der erlangten Information anstellen lässt. Die Musterlösung lautet: „Sich als das Mitglied ausgeben, das auf Seite V zugegriffen hat, und die Funktionen von Web-App Q nutzen.“
Laut Auswertungskommentar war die Erfolgsquote bei dieser Teilaufgabe hoch; es sei gut verstanden worden, welche Auswirkungen es hat, wenn ein Angreifer auf einer E-Commerce-Site an ein Cookie gelangt.
Und liest man die Spezifikation im Aufgabentext noch einmal, enthält die Mitgliedsprofilfunktion eine Seite zur Registrierung von Kreditkarteninformationen, ausdrücklich nur für eingeloggte Mitglieder zugänglich. Was am Ende der Identitätsübernahme steht, hatte der Aufgabentext von Anfang an bereitgestellt.
8. Warum es von der Site des Angreifers aus nicht funktioniert – Teilaufgabe 4
Teilaufgabe 4 ist die grundlegendste Frage dieser ganzen Aufgabe.
Angenommen, ein Angreifer bereitet auf einer Site unter einer selbst gewählten Domain HTML mit demselben Skript wie in Abbildung 4 vor, und ein eingeloggtes Mitglied von Web-App Q ruft diese Site auf. Der Angriff gelingt trotzdem nicht, aufgrund eines Mechanismus des Webbrowsers. Beschreiben Sie diesen Mechanismus in ≤40 Zeichen.
Die Musterlösung lautet: „Der Mechanismus, wonach vom Skript aus keine Cookies an eine URL einer anderen Domain gesendet werden.“
Angenommen, dasselbe Skript wird auf evil.example platziert und ein Mitglied von Unternehmen Q lockt man dorthin. Was passiert dann?
Zunächst geht die erste Kommunikation ins Leere. Sendet das Skript auf evil.example eine XMLHttpRequest an https://example.jp/user/profile, ist das eine Cross-Origin-Anfrage.
Entscheidend ist, dass kein Cookie angehängt wird. Das Skript in Abbildung 4 gibt withCredentials nicht an, sodass bei einer Cross-Origin-Anfrage kein Cookie mitgesendet wird. Aus Sicht des Servers von Unternehmen Q handelt es sich um einen nicht authentifizierten Zugriff, und er liefert keine Profileinstellungsseite mit Token zurück. Das Skript kann das Token nicht beschaffen und bleibt genau dort stehen.
Es gibt noch eine weitere Hürde: Die Antwort lässt sich nicht lesen. Erlaubt Unternehmen Q evil.example nicht über Access-Control-Allow-Origin, schlägt die CORS-Prüfung des Browsers fehl, der Browser behandelt die Kommunikation als Netzwerkfehler, onload feuert nicht, und xhr.response wird nie erreicht. Eine gewöhnliche Site gibt diesen Header für eine unbekannte Origin nicht zurück, sodass der Angriff in der Praxis auch hier stehen bleiben sollte.
Allerdings steht im Aufgabentext nicht, wie Unternehmen Q CORS konfiguriert hatte. Selbst wenn der Zugriff von evil.example erlaubt gewesen wäre, ließe sich nur die nicht authentifizierte Antwort lesen, die kein Token enthält – der Angriff gelingt also trotzdem nicht. Allein Punkt eins – kein angehängtes Cookie – stoppt den Angriff bereits.
Jeder der beiden Punkte für sich würde das Skript stoppen, tatsächlich wirken aber beide zugleich.
Zusätzlich lässt sich auch das Cookie selbst nicht lesen. Was document.cookie liefert, sind die Cookies der Origin, auf der das Skript läuft (evil.example). Die für example.jp ausgestellte Sitzungs-ID ist darin nicht enthalten.
IPAs Musterlösung zielt auf den erstgenannten Punkt ab, nämlich darauf, dass „Cookies nicht an eine URL einer anderen Domain gesendet werden“.
Merken Sie sich diese beiden Punkte hier nicht als denselben Mechanismus. Es sind getrennte Mechanismen.
- Dass
document.cookiedie Cookies vonexample.jpnicht zurückgibt, liegt daran, dass Cookies pro Domain getrennt verwaltet werden. Bei separaten Sites (separaten registrierbaren Domains) wieevil.exampleundexample.jplässt sich diese Trennung durch keine Konfiguration lockern. Anders bei Subdomains unter derselben Domain – über dasDomain-Attribut eines Cookies lässt sich ein Cookie zwischensub.example.jpundexample.jpteilen7 - Dass bei einer Cross-Origin-XHR kein Cookie angehängt wird, liegt daran, dass
withCredentialsstandardmäßigfalseist. Das kann sich ändern, wenn die passenden Bedingungen zusammenkommen: Gibt das SkriptwithCredentials = truean, erlaubt dasSameSite-Attribut des Cookies eine Cross-Site-Sendung, und gibt der Server überAccess-Control-Allow-Originausdrücklich die sendende Origin zusammen mitAccess-Control-Allow-Credentials: truezurück, dann wird das Cookie gesendet und die Antwort wird lesbar
Halten Sie hier den tatsächlichen Grund des Scheiterns und das, was bei einer Umschreibung nötig wäre, getrennt.
Aus dem Aufgabentext und Abbildung 4 lässt sich genau eines mit Sicherheit sagen: Da withCredentials nicht angegeben ist, wird kein Cookie angehängt, die Anfrage gilt als nicht authentifiziert, und das Token lässt sich nicht beschaffen. Allein das stoppt den Angriff.
Auch das Fehlen der CORS-Erlaubnis ist eine Hürde, die die Antwort unlesbar macht, doch da die Konfiguration von Unternehmen Q im Aufgabentext nicht angegeben ist, sollten Sie das als bedingte Verstärkung – „so wäre eine gewöhnliche Site konfiguriert“ – und nicht als Gewissheit behandeln.
Was passiert nun, wenn der Angreifer das Skript auf withCredentials = true umschreibt? Erst dann kommt eine weitere Bedingung ins Spiel. Erlaubt SameSite des Cookies keine Cross-Site-Sendung, wird dieses Cookie auch mit gesetztem withCredentials weiterhin nicht angehängt. Heutige Browser behandeln ein Cookie ohne angegebenes SameSite als Lax, sodass es standardmäßig zur Seite fällt, in der es nicht angehängt wird.
Beachten Sie, dass nicht angegeben ist, wie SameSite beim Sitzungs-ID-Cookie von Unternehmen Q konfiguriert war. Man kann also nicht sagen, dass „SameSite geschützt hat“. Es lässt sich nur sagen, dass SameSite gegenüber einem Angreifer, der mit Credentials angreift, wirken kann.
Zusammengefasst: Um von einer anderen Domain aus eine authentifizierte Antwort zu lesen, müssen alle drei Bedingungen zusammenkommen: withCredentials = true, ein SameSite des Cookies, das eine Cross-Site-Sendung erlaubt, und ein Server, der die sendende Origin per credentials-fähigem CORS erlaubt. Eine andere Domain bedeutet nicht automatisch Sicherheit, ist aber auch nicht schon gebrochen, sobald eine einzige Bedingung erfüllt ist. Verlassen Sie sich beim Prüfen der eigenen Site nicht allein auf die Domain-Trennung der Cookies, sondern prüfen Sie tatsächlich die CORS-Konfiguration (insbesondere, welchen Origins credentials-fähige Anfragen erlaubt sind) und das SameSite-Attribut der Cookies.
Deshalb ist Stored XSS für Angreifer so wertvoll
Diese Teilaufgabe zeigt, warum ein Angreifer darauf besteht, „eigenen Code innerhalb der Opfer-Site laufen zu lassen“.
Selbst wenn ein Angreifer eine täuschend echte Fake-Site auf der eigenen Domain vorbereitet, behandelt der Browser sie als separate Site. Über document.cookie lassen sich die Cookies der Zielsite nicht lesen, und bei einer XHR ohne Credentials, wie im Skript dieser Teilaufgabe, gilt man aus Sicht der Zielsite nicht einmal als eingeloggt. Der Wert für den Angreifer liegt genau darin, dass der Code auf der Origin der Opfer-Site läuft. Stored XSS ist genau das Mittel, das ermöglicht.
Lesen Sie das aber nicht als „von einer anderen Domain aus lässt sich überhaupt nie eine authentifizierte Anfrage senden“. Erlaubt das SameSite-Attribut des Cookies eine Cross-Site-Sendung (etwa SameSite=None), kann eine bösartige Seite trotzdem eine Anfrage mit angehängtem Cookie beim Zielserver zustellen lassen. Ein Formular-POST ist das klassische Beispiel dafür: Was CORS hauptsächlich kontrolliert, ist, „ob das Skript die Antwort lesen darf“ – nicht, ob die Anfrage den Server erreicht (allerdings läuft bei einem benutzerdefinierten Header oder beim Senden als application/json zunächst ein Preflight, und wird dieser nicht erlaubt, wird die eigentliche Anfrage gar nicht erst gesendet). Genau deshalb funktioniert der Angriff namens CSRF überhaupt, und genau deshalb braucht es ein CSRF-Token.
Mit anderen Worten: Was ein Cross-Origin-Skript standardmäßig nicht kann, ist, die Cookies der Gegenseite zu lesen und die Antwort zu lesen – nicht, eine Anfrage zu senden.
Und diese beiden Arten von „kann nicht lesen“ unterscheiden sich in ihrer Stärke. Die Domain-Trennung von document.cookie lässt sich durch keine serverseitige Konfiguration lockern, ob sich die Antwort lesen lässt, hängt aber von der CORS-Konfiguration des Zielservers ab. Erlaubt der Server die sendende Origin (gibt er bei credentials-fähigen Anfragen eine konkrete Origin in Access-Control-Allow-Origin zurück, zusammen mit Access-Control-Allow-Credentials: true), kann selbst ein Cross-Origin-Skript die Antwort lesen. Genau deshalb sollten Sie die CORS-Konfiguration der eigenen Site prüfen.
Was XSS besonders macht, ist, dass es all diese Bedingungen überspringt und von Anfang an als Origin der Opfer-Site auftreten kann.
Anders gesagt: XSS ist kein „Fehler, bei dem seltsame Zeichen auf dem Bildschirm erscheinen“, sondern ein Fehler, der den Angreifer in genau dieselbe Position wie einen regulären Nutzer der eigenen Site versetzt. Dieser Unterschied im Verständnis entscheidet über die Priorisierung.
9. Maßnahmen, die griffen, und Maßnahmen, die nicht griffen
Alles Bisherige fassen wir in einer einzigen Tabelle zusammen. Das ist der Teil dieses Artikels, den Sie am ehesten mitnehmen sollten.
| Was Unternehmen Q hatte/nicht hatte | Gegen diesen Angriff | Grund |
|---|---|---|
| Kommunikation über HTTPS | Griff nicht | Schützt den Kommunikationskanal, hat nichts mit einem in die Seite eingebetteten Skript zu tun |
| Eingabezeichenlimit von 50 Zeichen für den Titel / 300 Zeichen für den Text | Griff nicht | Wurde auf 15 Teile aufgeteilt gepostet, das dazwischenliegende HTML wurde als JavaScript-Kommentar überlesen |
| Upload-Token (CSRF-Schutz) | Griff nicht | Ein Skript auf derselben Origin kann sich das Token auf dem regulären Weg beschaffen |
| Login-Pflicht für die Upload-Funktion | Griff nicht | Ausgeführt wird es im eigenen Browser des bereits eingeloggten Opfers |
| Escaping bei der Ausgabe (fehlte) | Wirksam (grundlegende Lösung) | Die geposteten Zeichenketten werden nicht mehr als HTML interpretiert, das Skript läuft also gar nicht erst |
| HttpOnly-Attribut des Cookies (fehlte) | Wirksam (Ausweichmaßnahme) | Die Sitzungs-ID lässt sich nicht mehr über document.cookie lesen |
| Formatprüfung und Reencoding hochgeladener Dateien (fehlte) | Wirksam (Ausweichmaßnahme) | Roher Text lässt sich nicht als PNG speichern, und Reencoding löscht auch in Metadaten versteckte Zeichenketten. Bleibt allerdings erhalten, wenn sie als Pixel des Bildes selbst kodiert ist |
Die oberen vier Zeilen hatten gegen diesen Angriff keinerlei Wirkung. Ordnet man die unteren drei den Kategorien aus IPAs „So sichern Sie Ihre Website“ zu, ist die erste die grundlegende Lösung, die übrigen beiden sind Ausweichmaßnahmen5.
Eine Anmerkung dazu: Die oberen vier bedeuten nicht, dass die IPA sie nicht als Gegenmaßnahme anerkennt. IPAs Einteilung kennt nur zwei Kategorien, grundlegende Lösung und Ausweichmaßnahme, und die inhaltliche Prüfung von Eingabewerten ist dort sogar als Ausweichmaßnahme aufgeführt. Allerdings mit dem Hinweis, dass „die Wirksamkeit dieser Gegenmaßnahme begrenzt ist“ – und genau dieser Grenzfall trifft hier zu.
Und wichtig ist: Hätte auch nur eine der unteren drei bestanden, wäre die in dieser Aufgabe ausgeführte Angriffskette irgendwo abgerissen.
- Mit Escaping würde das Skript erst gar nicht ausgeführt
- Mit HttpOnly würde das Skript zwar ausgeführt, könnte aber das Cookie nicht lesen
- Mit Formatprüfung ließe sich das Cookie zwar lesen, roher Text ließe sich aber nicht als PNG hochladen
Genau in solchen Situationen entfaltet gestaffelte Verteidigung ihren Sinn. Ausweichmaßnahmen ersetzen aber trotzdem nicht die grundlegende Lösung. Selbst mit gesetztem HttpOnly kann ein Angreifer, solange das XSS bestehen bleibt, im Browser des Opfers beliebige Aktionen ausführen – Produkte kaufen, registrierte Daten ändern und Ähnliches. Angriffe, die nicht einmal die Sitzungs-ID stehlen müssen, lassen sich beliebig viele entwerfen.
Warum in der Tabelle nicht nur „Formatprüfung“, sondern auch „Reencoding“ steht
Dass die letzte Zeile der Tabelle zweistufig als „Formatprüfung und Reencoding“ geschrieben ist, hat einen Grund.
Unternehmen Q hat im Aufgabentext das Format der Bilddatei überhaupt nicht geprüft, sodass allein eine Formatprüfung diesen Angriff gestoppt hätte. Das gilt aber nur, wenn der Angreifer rohen Text sendet. Ein Angreifer kann ebenso gut eine reguläre PNG-Datei zusammensetzen und die Zeichenkette in deren Metadatenbereich verstecken. In diesem Fall würde die Prüfung „ist das Format korrekt“ anstandslos bestehen.
Um diesen Weg einzuengen, braucht es also zusätzlich, das hochgeladene Bild serverseitig neu zu kodieren und erst dann auszuliefern (die ursprüngliche Bytefolge nicht unverändert zu speichern).
Auch Reencoding schließt das aber nicht vollständig. Malt der Angreifer die Sitzungs-ID als Pixel selbst in ein reguläres PNG, bleibt diese sichtbare Information bei einem gewöhnlichen Reencoding erhalten, sodass der Angreifer sie weiterhin herunterladen und auslesen kann. Was Reencoding stoppt, ist die Variante mit roher Bytefolge und die Variante, die im Metadatenbereich versteckt wird – nicht Information, die als sichtbarer Inhalt des Bildes kodiert ist.
Kurz: Solange es auf der Site einen Ort gibt, der zugleich beschreib- und lesbar ist, lässt er sich als Abflussweg nie vollständig schließen. Das zeigt die Natur von Ausweichmaßnahmen gut. Es reicht nicht, dass eine Ausweichmaßnahme „den gerade beobachteten Angriff“ stoppt – sie muss so entworfen sein, dass geprüft wird, ob sie auch bei einer leicht veränderten Methode des Angreifers noch hält. Und egal, wie viele man aufeinanderstapelt, sie ersetzen nie die grundlegende Lösung: das Escaping bei der Ausgabe.
Noch eine Anmerkung: Ein Entwurf, bei dem hochgeladene Dateien von einer separaten Domain ausgeliefert werden, wird oft empfohlen, ist aber keine Gegenmaßnahme gegen genau diesen Abflussweg. Der Angreifer kann das Bild von einer anderen Domain aus genauso herunterladen, denn die Same-Origin-Policy ist kein Mechanismus, der eine öffentlich ausgelieferte Bytefolge geheim hält. Wirksam ist eine separate Auslieferungsdomain gegen die Bedrohung, dass die hochgeladene Datei selbst als aktiver Inhalt auf der Origin der Opfer-Site ausgeführt wird (etwa beim Hochladen von HTML oder SVG). Verstehen Sie das als Gegenmaßnahme gegen eine andere Bedrohung und halten Sie beides auseinander.
Der Grundgedanke, eine Datei nicht nach der Angabe des Absenders (Dateiendung oder Content-Type), sondern nach dem tatsächlichen Inhalt zu beurteilen, ist eine grundlegende Praxis, die weit über das Web hinaus gilt. Was „nach dem Inhalt beurteilen“ bedeutet, unterscheidet sich aber je nach Format. Bei einem Format wie PNG, das am Anfang eine feste Bytefolge besitzt, lässt sich das prüfen. Bei CSV gibt es dagegen keine standardisierte Signatur; eine führende BOM zeigt nur die Zeichenkodierung an und ist kein Beweis dafür, dass der Inhalt tatsächlich CSV ist. Wie in CSV ist nicht „nur Text“ ── CSV in der Praxis für C#-Business-Anwendungen (Zeichenkodierung, Excel-Kompatibilität, Schutz vor Injection) behandelt, läuft die Prüfung bei CSV stattdessen darauf hinaus, ob sich die Datei unter dem erwarteten Dialekt fehlerfrei parsen lässt und ob sie innerhalb des erwarteten Schemas und der erwarteten Zeilenobergrenze bleibt.
10. Was Sie im eigenen Code prüfen sollten
Übersetzt man diese ganze Aufgabe in eine Checkliste fürs Code-Review, ergibt sich Folgendes. Jede Web-Anwendung mit Mitglieder- oder Beitragsfunktion kann sie unverändert nutzen.
- Greift das Escaping bei der Ausgabe an jeder einzelnen Ausgabestelle? Listen Sie alle Stellen auf, an denen das automatische Escaping der Template-Engine ausdrücklich deaktiviert wurde (
raw,| safe,dangerouslySetInnerHTMLund Ähnliches), und prüfen Sie für jede, ob sich der Grund für diese Deaktivierung erklären lässt - Zählen Sie Beschränkungen der Eingabeseite nicht als XSS-Gegenmaßnahme? Zeichenlimits und Beschränkungen der erlaubten Zeichenarten sind Anforderungen der Spezifikation, keine grundlegende Lösung gegen XSS
- Escapen Sie nicht bei der Eingabe? Die Rolle der Eingabeseite ist Validierung, die Werte außerhalb der Spezifikation zurückweist – nicht Escaping. Zum Zeitpunkt der Eingabe steht das Ziel (HTML, CSV, JSON, E-Mail, Log) noch nicht fest, sodass Escaping bei der Eingabe sowohl zu doppeltem Escaping als auch zu Datenzerstörung führt. Dasselbe gilt für den Aufbau von SQL-Anweisungen – die grundlegende Lösung dort sind Platzhalter
- Sind bei Cookies
HttpOnly,SecureundSameSitegesetzt? Prüfen Sie insbesondere das Sitzungs-ID-Cookie - Wird das Format hochgeladener Dateien anhand des tatsächlichen Inhalts geprüft, nicht anhand von Dateiendung oder Content-Type? Vom Absender angegebene Information taugt nicht als Prüfgrundlage. Allerdings lässt allein eine Formatprüfung noch den Trick durch, Daten im Metadatenbereich eines regulären Bildes zu verstecken
- Werden hochgeladene Bilder vor der Auslieferung neu kodiert? Prüfen Sie, ob die ursprüngliche Bytefolge nicht unverändert zurückgegeben wird. Beachten Sie aber, dass als Pixel kodierte Information auch Reencoding übersteht, sodass sich ein „beschreib- und lesbarer“ Ort nie vollständig schließen lässt. Auslieferung von einer separaten Domain wiederum ist eine Gegenmaßnahme gegen die Bedrohung, dass eine hochgeladene Datei auf der eigenen Origin ausgeführt wird – nicht gegen diesen Abflussweg
- Ist die CSP so konfiguriert, dass sie Inline-Skripte stoppen kann? Das Kriterium ist nicht, „ob
'unsafe-inline'dasteht“, sondern „ob ein Inline-Skript tatsächlich erlaubt ist“. Enthältscript-srceine Angabe von Nonce oder Hash, ignorieren Browser ab CSP Level 2'unsafe-inline'– bei einer abwärtskompatiblen Policy, die'unsafe-inline'und Nonce gemeinsam schreibt, wird ein eingeschleustes Skript ohne Nonce also weiterhin blockiert - Können Sie erklären, „was“ ein Token eigentlich schützt? Ein CSRF-Token verhindert kein XSS. Beide brauchen jeweils eine eigene Gegenmaßnahme
Besonders Punkt 1 ist ein typisches Muster, das bei tatsächlichen Untersuchungen auftaucht. Man hielt sich für sicher, weil das Framework automatisch escaped – hatte dieses automatische Escaping aber für eine Einzelanforderung wie „hier soll rohes HTML eingefügt werden“ an genau einer Stelle deaktiviert. Das ist keine Seltenheit.
Schluss – Die eigentliche Fähigkeit, die diese Aufgabe prüft
Die bisher besprochene Liste der Gegenmaßnahmen steht zusammengefasst am Anfang, unter „Zunächst das Ergebnis“. Zum Abschluss noch ein letzter Gedanke zu dieser Aufgabe selbst.
Was diese Prüfungsaufgabe gut macht, ist, dass sie nicht fragt „kennen Sie XSS?“, sondern „können Sie unter den vorhandenen Gegenmaßnahmen erkennen, welche tatsächlich wirken?“
Unternehmen Q hat nicht nichts getan. Es kommunizierte über HTTPS, setzte ein Zeichenlimit für die Eingabe, verlangte für Uploads ein Token und beschränkte die Funktion auf eingeloggte Mitglieder. Nebeneinander gestellt, wirkt das wie eine ordentlich abgesicherte Konstruktion. Trotzdem wurde alles davon umgangen. Umgekehrt sind die drei fehlenden Punkte allesamt unauffällig – keiner davon ist etwas, das man in Release Notes schreiben würde.
Was Sicherheitsdiskussionen schwierig macht, ist, dass die Anzahl der Gegenmaßnahmen nicht proportional zur Stärke der Verteidigung ist. Es zählt nicht „was tut man alles“, sondern ob man Stück für Stück sagen kann, „welchen Angriff, an welcher Stelle, diese Gegenmaßnahme stoppt“. Das ist keine Fähigkeit, die nur für eine Zertifizierungsprüfung nötig ist – es ist die Urteilsfähigkeit, die sowohl bei der Entgegennahme einer Sicherheitserklärung als auch bei ihrer Umsetzung am meisten gebraucht wird.
Verwandte Beratungsbereiche
Die KomuraSoft LLC übernimmt sowohl die Entwicklung von Websites mit Mitglieder- und Beitragsfunktion als auch das Design-Review bestehender Web-Anwendungen darauf, ob dieselbe Lücke offensteht.
- Website-Erstellung und -Relaunch
- Technikberatung / Design-Review
- Fehleruntersuchung & Ursachenanalyse
- Kontakt
Quellen
-
IPA (Information-technology Promotion Agency, Japan), das „Aufgabenheft der Nachmittagssitzung der Prüfung zum Registered Information Security Specialist, Herbst 2023 (Reiwa 5)“, enthalten in Aufgabenhefte, Punkteverteilung, Musterlösungen und Auswertungskommentare (Geschäftsjahr 2023, Reiwa 5). Behandelt: die Funktionen von Unternehmen Qs Web-App Q (Mitgliedsregistrierung, Login und Ausgabe der Sitzungs-ID per Cookie, die Produktbewertungsfunktion mit dem 50-Zeichen-Limit für den Bewertungstitel und dem 300-Zeichen-Limit für den Bewertungstext, die Mitgliedsprofilfunktion mit Avatar-Bild-Upload und Registrierung von Kreditkarteninformationen, dass der Avatar-Bild-Upload eine Bilddatei und ein Token als Parameter verwendet, und dass ein hochgeladenes Avatar-Bild auf der Profileinstellungsseite des Mitglieds sowie auf Bewertungsseiten angezeigt wird); das Phänomen, dass Seite V nur 2 von 16 Bewertungen zeigt; dass das HTML von Seite V 15 Beiträge von Mitglied A samt eines langen Skripts enthielt; den Inhalt des extrahierten Skripts; sowie die Schwachstelle in Web-App Q, wonach ein von einem Mitglied eingegebenes Skript ausgeführt wird, das Fehlen des HttpOnly-Attributs beim Cookie und das Fehlen einer Formatprüfung bei hochgeladenen Bilddateien. Auch der Wortlaut der Teilaufgaben 1 bis 4 stammt aus diesem Heft. ↩ ↩2
-
IPA, Häufig gestellte Fragen zu den Prüfungen. Behandelt, dass für die Nutzung von IPAs veröffentlichten früheren Prüfungsaufgaben, sofern gesetzlich nichts anderes bestimmt ist, weder Genehmigung noch Nutzungsgebühr erforderlich ist; dass dies nicht bedeutet, dass auf das Urheberrecht verzichtet wird; dass die Quelle in der Form „Geschäftsjahr, Prüfungstermin, Prüfungskategorie, Zeitabschnitt, Aufgabennummer usw.“ angegeben werden muss (als Beispiel wird „Quelle: Prüfung zum Fundamental Information Technology Engineer, Frühjahr 2019 (Heisei 31), Vormittag, Aufgabe 1“ genannt); und dass auch eine Veränderung an einer Aufgabe entsprechend vermerkt werden muss. ↩
-
IPA, Prüfung zum Registered Information Security Specialist, Herbst 2023 (Reiwa 5) – Musterlösungen. Behandelt das erklärte Prüfungsziel von Aufgabe 1 (anhand eines Vorfalls, der durch die Ausnutzung einer Schwachstelle in einem Webanwendungsprogramm entstand, die Fähigkeit zu prüfen, aus HTML und ECMAScript die ausgenutzte Schwachstelle und das Problem zu lesen und eine Gegenmaßnahme zu entwerfen) sowie die Musterlösung jeder Teilaufgabe (Teilaufgabe 1(1): „Stored XSS“ – Option イ im Heft; Teilaufgabe 1(2): „Escaping auf den Bewertungstitel anwenden, bevor er ausgegeben wird.“; Teilaufgabe 2: „Das HTML wurde durch mehrfach aufgeteilte Beiträge auskommentiert, sodass daraus ein einziges Skript wurde.“; Teilaufgabe 3(1): „Lädt die Sitzungs-ID zusammen mit dem aus der XHR-Antwort erhaltenen Token als Avatar-Bild hoch.“; Teilaufgabe 3(2): „Lädt das Avatar-Bild des Mitglieds herunter und liest daraus die Zeichenkette der Sitzungs-ID aus.“; Teilaufgabe 3(3): „Sich als das Mitglied ausgeben, das auf Seite V zugegriffen hat, und die Funktionen von Web-App Q nutzen.“; Teilaufgabe 4: „Der Mechanismus, wonach vom Skript aus keine Cookies an eine URL einer anderen Domain gesendet werden“). ↩ ↩2
-
IPA, Prüfung zum Registered Information Security Specialist, Herbst 2023 (Reiwa 5) – Auswertungskommentar. Behandelt: dass die Gesamterfolgsquote von Aufgabe 1 durchschnittlich war; dass zu Teilaufgabe 1(1) angemerkt wird, „die Erfolgsquote war durchschnittlich, aber es fielen einige Prüflinge auf, die – wohl weil das Skript das DOM verwendete – fälschlicherweise mit ‚DOM Based XSS‘ antworteten“; den Hinweis, dass Prüflinge Schwachstellen einschließlich ihrer Merkmale und Gegenmaßnahmen genau verstehen sollten; dass zu Teilaufgabe 2 angemerkt wird, „vereinzelt waren Antworten zu sehen, die auf eine unzureichende Prüfung schließen ließen, etwa ‚die Eingabebeschränkung wurde vor dem Posten über die Entwicklertools entfernt‘“; den Hinweis, dass Prüflinge die Fähigkeit entwickeln sollten, vom Angreifer hinterlassene Spuren sorgfältig zu prüfen und die Angriffsmethode korrekt zu erfassen; sowie dass die Erfolgsquote bei Teilaufgabe 3(3) hoch war und gut verstanden wurde, welche Auswirkungen es hat, wenn ein Angreifer auf einer E-Commerce-Site an ein Cookie gelangt. ↩ ↩2
-
IPA, „So sichern Sie Ihre Website“. Behandelt, dass die Bedrohungen und Gegenmaßnahmen zu 11 Kategorien von Website-Schwachstellen in „grundlegende Lösung“ (eine Umsetzung, die die eigentliche Ursache der Schwachstelle beseitigt) und „Ausweichmaßnahme“ (eine Gegenmaßnahme, die die Erfolgsquote oder Auswirkung eines Angriffs mindert, wenn eine Schwachstelle bestehen bleibt) unterteilt dargestellt werden; dass als grundlegende Lösung gegen Cross-Site-Scripting genannt wird, auf alle auf der Webseite ausgegebenen Elemente ein Escaping anzuwenden; sowie die begleitende Checkliste zur Sicherheitsumsetzung und die beiden Zusatzbände „So rufen Sie SQL sicher auf“ und „Spezifikation für den Sicherheits-Gesundheitscheck von Websites“. ↩ ↩2 ↩3
-
IPA, „So sichern Sie Ihre Website“ – 1.1 SQL-Injection. Behandelt, dass als grundlegende Lösung gegen SQL-Injection genannt wird, „den Aufbau von SQL-Anweisungen ausnahmslos mit Platzhaltern zu implementieren“, wobei statische Platzhalter (Prepared Statements) als prinzipiell unfähig gelten, die Schwachstelle entstehen zu lassen; dass für die Umsetzung, wenn SQL-Anweisungen zwangsläufig durch Zeichenkettenverkettung aufgebaut werden müssen, geschrieben steht: „Erfolgt der Aufbau der SQL-Anweisung durch Zeichenkettenverkettung, ist eine API der Datenbank-Engine zu verwenden, die ein Escaping durchführt, um die Literale der SQL-Anweisung korrekt zu bilden“ – das Escaping also am Aufbau der Literale der SQL-Anweisung erfolgt, nicht zum Zeitpunkt des Empfangs der Eingabe; sowie dass daneben als weitere grundlegende Lösung „SQL-Anweisungen nicht direkt in an die Webanwendung übergebenen Parametern angeben“ und als Ausweichmaßnahmen „Fehlermeldungen nicht unverändert im Browser anzeigen“ und „dem Datenbankkonto nur angemessene Berechtigungen einräumen“ genannt werden. ↩
-
IETF, RFC 6265: HTTP State Management Mechanism, Abschnitt 4.1.2.6, „The HttpOnly Attribute“. Dazu, dass das HttpOnly-Attribut den Geltungsbereich eines Cookies auf HTTP-Anfragen beschränkt und insbesondere den User-Agent anweist, das Cookie auszulassen, wenn Zugriff auf Cookies über „Nicht-HTTP“-APIs gewährt wird, etwa eine Web-Browser-API, die Cookies gegenüber Skripten offenlegt. Außerdem zum Secure-Attribut (Abschnitt 4.1.2.5), das ein Cookie darauf beschränkt, nur über einen sicheren Kanal gesendet zu werden. ↩ ↩2
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
Prüfung zum Registered Information Security Specialist – Frühjahr 2024 (Reiwa 6), Nachmittag, Aufgabe 1 erklärt – JWT alg=none, API-Autorisierung und vorläufige WAF-Abwehr
Anhand von Aufgabe 1 der Nachmittagsprüfung Frühjahr 2024 (Reiwa 6) zum Registered Information Security Specialist erklärt dieser Artikel...
Prüfung zum Registered Information Security Specialist – Herbst 2023 (Reiwa 5), Nachmittag, Aufgabe 2 erklärt – Dateien, die über das Gäste-WLAN entwendet werden
Anhand von Aufgabe 2 der Nachmittagsprüfung Herbst 2023 (Reiwa 5) zum Registered Information Security Specialist erklärt dieser Artikel, ...
Was auch Website-Auftraggeber wissen sollten ── Die IPA-Anleitung „So sichern Sie Ihre Website“ als Checkliste nutzen
An welchem Maßstab sollten Sie die Sicherheit der Website Ihres Unternehmens prüfen? Dieser Artikel erklärt die 11 Schwachstellen und Geg...
Die 10 größten Bedrohungen der Informationssicherheit 2026 — Wie man das Ranking liest und was KMU wirklich absichern sollten
In IPAs „10 größte Bedrohungen der Informationssicherheit 2026“ belegten Ransomware-Angriffe zum 11. Mal in Folge Platz 1, Supply-Chain-A...
Wo sollten KMU bei der Sicherheit anfangen? — Ein Durchgang durch die IPA-„Informationssicherheitsleitlinien für KMU“, 4. Ausgabe
Wo sollten kleine und mittlere Unternehmen bei der Sicherheit anfangen? Anhand der IPA-„Informationssicherheitsleitlinien für kleine und ...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Website-Entwicklung
Bei Websites mit Mitgliederfunktion und Beitragsfunktion wirken sich die in diesem Artikel behandelten Punkte – etwa Escaping bei der Ausgabe und die Konfiguration von Cookie-Attributen – unmittelbar auf die Qualität der Umsetzung aus.
Technische Beratung und Design-Review
Herauszufinden, wo in einer bestehenden Web-Anwendung dieselbe Lücke klafft, ist als Design-Review genau der Bereich unserer technischen Beratung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Warum ist das XSS in dieser Aufgabe „Stored XSS“? Das Skript manipuliert doch das DOM – ist das nicht DOM Based XSS?
- Die Art der Schwachstelle wird dadurch bestimmt, „wo“ die Zeichenkette des Angreifers zu ausführbarem Code wird – also ob sich die verwundbare Ausgabestelle (die Senke) auf der Serverseite oder auf der Browserseite befindet. In dieser Aufgabe hat der Server den vom Angreifer geposteten Bewertungstitel unverändert in HTML eingebettet und zurückgegeben. Die Senke liegt in der Ausgabeverarbeitung des Servers, und die Zeichenkette wird gespeichert und danach an alle verteilt, die die Seite öffnen – deshalb handelt es sich um Stored XSS. DOM Based XSS bezeichnet die Kategorie, bei der browserseitiges JavaScript einen Wert erst ausführbar macht, etwa durch eine Zuweisung an innerHTML. Ob das eingebettete Skript DOM-APIs wie XMLHttpRequest oder getElementById verwendet, hat nichts mit der Art der Schwachstelle zu tun. Selbst wenn der Server einen gespeicherten Wert als harmlosen Text oder als JSON zurückgibt, wird daraus DOM Based XSS, sobald browserseitiges JavaScript ihn an innerHTML übergibt. Entscheiden Sie nicht danach, ob die Zeichenkette in der Antwort sichtbar ist, sondern danach, wo sie ausführbar wurde. Auch IPAs Auswertungskommentar merkt an, dass mehrere Prüflinge – wohl weil das Skript das DOM verwendete – fälschlicherweise „DOM Based XSS“ geantwortet haben.
- Der Bewertungstitel hatte doch ein Eingabelimit von 50 Zeichen – warum konnte trotzdem ein langes Skript ausgeführt werden?
- Der Angreifer hat die Nutzlast in Längen aufgeteilt, die jeweils in einen einzelnen Beitrag passen, und sie über mehrere Beiträge verteilt gepostet. Der erste Beitrag öffnet mitten im Titel ein script-Tag und öffnet am Ende des Titels einen JavaScript-Blockkommentar (Schrägstrich und Sternchen). Der zweite Beitrag schließt diesen Kommentar an seinem Anfang, schreibt die nächste Anweisung und öffnet am Ende wieder einen Kommentar – dieses Muster wiederholt sich. Dadurch landet das gesamte HTML, das zwischen den Beiträgen liegt (div-Tags und Ähnliches), innerhalb eines JavaScript-Kommentars und wird ignoriert, sodass nur die aufgeteilten Fragmente aneinandergereiht als ein einziges Skript ausgeführt werden. Das Eingabezeichenlimit hat nur die Länge des einzelnen Beitrags begrenzt – nicht die Anzahl der Beiträge.
- Wohin wurde die gestohlene Sitzungs-ID gesendet?
- An keinen externen Server. Das Skript hat den Inhalt des Cookies unverändert zum Inhalt einer Datei gemacht, sie „a.png“ genannt, den MIME-Typ auf „image/png“ gesetzt und an die Avatar-Bild-Upload-Funktion genau der E-Commerce-Site gesendet, die das Opfer selbst nutzt. Da das hochgeladene Avatar-Bild unter anderem auf der Bewertungsseite angezeigt wird, konnte der Angreifer dieses Bild ganz normal herunterladen und die Sitzungs-ID aus dem Inhalt herauslesen. Der Browser des Opfers kommuniziert dabei ausschließlich mit der legitimen Site; es entsteht keine auffällige externe Kommunikation, weshalb URL-Filterung am Proxy oder die Überwachung des ausgehenden Datenverkehrs an der Firewall diesen Weg nicht bemerken.
- Für den Upload war doch ein Token nötig – warum konnte der Angriff trotzdem gelingen?
- Weil dieses Token ein Mechanismus ist, um unautorisierte Anfragen von einer anderen Site abzuwehren (ein CSRF-Schutz) – es war nie für ein Skript gedacht, das auf derselben Site läuft. Das Angriffsskript greift zunächst per XMLHttpRequest auf die Profileinstellungsseite zu, erhält dort das Token auf demselben Weg wie der reguläre Bildschirm, und führt den Upload dann mit diesem Token aus. Weil XSS den Code des Angreifers auf derselben Origin wie das Opfer ausführt, kann er alles tun, was ein regulärer Nutzer auch kann. Ein CSRF-Token verhindert kein XSS.
- Ich habe gehört, dass bei SQL-Injection das Escaping der Eingabe wichtig ist. Ist Escaping bei der Ausgabe nur bei XSS relevant?
- Nein, auch bei SQL-Injection ist „Escaping bei der Eingabe“ keine Gegenmaßnahme. Was die IPA als grundlegende Lösung nennt, ist, „den Aufbau von SQL-Anweisungen ausnahmslos mit Platzhaltern zu implementieren“. Für den Fall, dass eine SQL-Anweisung durch Zeichenkettenverkettung aufgebaut wird, wird Escaping als Alternative genannt – aber auch dort geht es darum, „die Literale der SQL-Anweisung korrekt zu bilden“, also um einen Schritt, der im Moment des Aufbaus der SQL-Anweisung erfolgt, nicht zum Zeitpunkt der Eingabe. Das gemeinsame Prinzip lautet: „Escaping geschieht dort, wo entschieden wird, in welche Grammatik die Daten hinausgehen.“ Geht es als HTML hinaus, wird HTML-Escaping verwendet; wird es Teil einer SQL-Anweisung, werden Platzhalter verwendet; wird es Teil eines Shell-Befehls, wird es nach den Regeln der Shell behandelt. Der Grund, warum bei der Eingabe nicht escaped werden darf, ist, dass zu diesem Zeitpunkt das Ziel der Ausgabe noch nicht feststeht. Dieselben Daten gehen als HTML-Seite, als CSV, als JSON, als Benachrichtigungs-E-Mail und als Log-Eintrag hinaus. Wird bei der Eingabe HTML-Escaping angewendet, erscheinen in der CSV unveränderte Entity-Referenzen wie `&`, und der Datenbankinhalt weicht von der ursprünglichen Eingabe ab, wodurch Suche und Auswertung durcheinandergeraten. Das heißt nicht, dass die Eingabeseite gar nichts tun soll. Ihre Aufgabe ist nicht Escaping, sondern Validierung: Werte zurückzuweisen, die nach der Spezifikation gar nicht vorkommen dürfen.
- Welche Gegenmaßnahme sollte man aus dieser Aufgabe für die eigene Praxis mitnehmen?
- Die grundlegende Lösung besteht darin, bei sämtlichen Nutzereingaben – einschließlich des Bewertungstitels – unmittelbar vor der Ausgabe als HTML ein Escaping durchzuführen. Als Ausweichmaßnahmen kommen ergänzend hinzu: das HttpOnly-Attribut auf dem Sitzungs-ID-Cookie setzen, damit es sich nicht per Skript auslesen lässt; hochgeladene Bilder serverseitig neu kodieren, damit die ursprüngliche Bytefolge nicht erhalten bleibt; und die Content Security Policy so konfigurieren, dass sich die Ausführung von Inline-Skripten unterbinden lässt. Hätte Unternehmen Q in dieser Aufgabe auch nur eine dieser Maßnahmen umgesetzt, wäre die Angriffskette irgendwo abgerissen. Ausweichmaßnahmen lassen sich jedoch umgehen, sobald der Angreifer seine Methode ändert, und sind daher kein Ersatz für die grundlegende Lösung des Escapings.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.