DOM-based XSS ist die XSS-Variante, die den Server niemals erreicht. Sie passiert vollständig im Browser — typischerweise, wenn JavaScript einen Wert aus dem URL-Fragment, aus localStorage, aus postMessage oder einer ähnlichen client-seitigen Quelle nimmt und ihn in einen gefährlichen DOM-Sink schreibt. Server-Logs zeigen nichts. WAFs sehen nichts. Klassische Eingabe-Validierung wirkt nicht. Dieser Artikel zeigt das Source-Sink-Modell, die häufigsten Vektoren und die modernen Browser-Antworten.
Was DOM-XSS unterscheidet
Bei Reflected/Stored XSS schreibt der Server schadhafter HTML in die Antwort. Bei DOM-XSS liest Client-JavaScript einen Wert aus einer Source und schreibt ihn in einen Sink, ohne dass der Server beteiligt ist.
Konkretes Beispiel:
// Single-Page-App liest URL-Hash und schreibt es in den DOM
const hash = window.location.hash.slice(1); // Source
document.getElementById('content').innerHTML = `Willkommen, ${hash}!`; // SinkURL: https://app.example/#<img src=x onerror=alert(1)> → Browser navigiert, JS liest den Hash, schreibt ihn als HTML in den DOM. Browser parst das HTML, rendert das <img>-Tag, lädt das (nicht existierende) Bild, triggert onerror, führt alert(1) aus.
Server hat das Skript nie gesehen: der URL-Fragment (#...) wird vom Browser nicht zum Server geschickt. Server-Logs zeigen den Aufruf der Seite, aber nicht den Hash. WAFs filtern nichts.
Folgerungen:
- Eingabe-Validierung auf Server-Seite schützt nicht.
- WAFs schützen nicht.
- Output-Encoding im Server-Template schützt nicht.
Die einzige wirksame Verteidigung gegen DOM-XSS liegt im Client-Code: Sources sicher verarbeiten, Sinks vermeiden oder sichern.
Sources: woher kommt User-Input im Client?
DOM-XSS-Sources sind alle Wege, über die User-kontrollierte Daten in JS-Variablen landen:
window.location.hash— URL-Fragment.window.location.search— URL-Query-String.window.location.pathname— URL-Pfad.document.URL,document.documentURI— komplette URL.document.referrer— vorherige URL.window.name— Cross-Frame-Daten.postMessage-Events — Cross-Origin-Daten.localStorage,sessionStorage,IndexedDB— wenn der Inhalt von User-kontrolliert ist.- Cookies — wenn von User-Input gespeist.
<input>-Felder, Form-Daten — Live-Input während der Sitzung.- WebSockets, Server-Sent Events — Live-Daten-Streams.
Jede dieser Quellen kann von einer Angreifer:in beeinflusst werden — entweder direkt (URL) oder indirekt (vorherige XSS, kompromittierter Server, Cross-Site-Postnachricht).
Sinks: wo wird's gefährlich?
Sinks sind APIs, die Strings als Code oder als HTML interpretieren:
Direkte HTML-Insertion:
element.innerHTML = ...— klassischster Sink.element.outerHTML = ....element.insertAdjacentHTML(...).document.write(...),document.writeln(...).
Attribut-Setter mit JS-Interpretation:
element.setAttribute('href', '...')— wenn die URL mitjavascript:beginnt, wird das beim Klick als Code ausgeführt.element.setAttribute('src', '...')in Script-Tags.
Direkter Code-Eval:
eval(...)— String wird als JS ausgewertet.Function(...)— Konstruktor, der String als Funktion erzeugt.setTimeout('code', ...),setInterval('code', ...)mit String-Argument (deprecated, aber existiert).
DOM-Manipulation mit interpretiertem Inhalt:
element.style = '...'— CSS-Expressions sind in modernen Browsern weitgehend deaktiviert, aber CSS-Imports und URL-Werte sind weiter Sinks.
Frame-Sinks:
iframe.src = '...'—javascript:-URLs werden ausgeführt.window.open(url, ...)mitjavascript:-URL.location = '...',location.href = '...'—javascript:-URLs.
Storage-Sinks (indirekt):
- Werte in
localStorageschreiben, die später in einen anderen Sink fließen.
| Sink-Kategorie | Beispiele | Schutz |
|---|---|---|
| HTML-Insertion | innerHTML, outerHTML, document.write | Sanitization oder textContent statt innerHTML |
| URL-Setter | href, src, location | URL-Schema validieren (kein javascript:) |
| Code-Eval | eval, Function, setTimeout('str') | Komplett vermeiden |
| CSS-Insertion | element.style = ... mit url(...) | CSS-Werte validieren |
Konkrete Sink-Patterns
innerHTML als häufigster Sink:
// Schadhaft
const name = new URLSearchParams(location.search).get('name');
document.querySelector('#greeting').innerHTML = `Hallo, ${name}!`;
// Sicher (textContent statt innerHTML)
document.querySelector('#greeting').textContent = `Hallo, ${name}!`;
// Sicher (Sanitization)
const clean = DOMPurify.sanitize(name);
document.querySelector('#greeting').innerHTML = `Hallo, ${clean}!`;location.href-Setter mit User-URL:
// Schadhaft: javascript:-URLs möglich
const next = new URLSearchParams(location.search).get('next');
location.href = next;
// Sicher: URL-Schema validieren
const next = new URLSearchParams(location.search).get('next');
try {
const url = new URL(next, location.origin);
if (url.protocol === 'https:' || url.protocol === 'http:') {
location.href = url.href;
}
} catch { /* invalid URL */ }postMessage-Empfang ohne Origin-Check:
// Schadhaft: nimmt jede Nachricht entgegen
window.addEventListener('message', (event) => {
document.body.innerHTML = event.data; // jeder Iframe kann das triggern
});
// Sicher: Origin prüfen
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted.example') return;
// ... immer noch nicht direkt innerHTML, lieber strukturierte Daten
});Warum SPAs besonders betroffen sind
Single-Page-Apps (React, Vue, Angular, Svelte) verarbeiten viel mehr im Client als Server-rendering-Apps. Daher haben sie strukturell mehr DOM-XSS-Risiko:
- Client-Routing liest URL-Fragmente und -Parameter, rendert basierend darauf.
- Lokale State-Verwaltung speichert User-Input in Redux/Pinia/etc., rendert mehrfach.
- Komplexe Client-seitige Logik mit vielen Templating-Pfaden.
- Drittanbieter-Komponenten (UI-Libraries, Analytics, Widgets) mit eigenem HTML-Output.
Aber: SPAs nutzen typischerweise Frameworks mit Auto-Encoding. Das wirkt gegen klassische innerHTML-Sinks — solange du nicht aktiv dangerouslySetInnerHTML, v-html, [innerHTML], {@html} nutzt.
Die größten DOM-XSS-Risiken in modernen SPAs:
- Explizite Roh-HTML-Insertion in Markdown-Renderern, Rich-Text-Editoren, CMS-Content.
- URL-Parameter in Click-Handlern für Redirects.
- iframe-
src-Setter in eingebetteten Widgets. - Drittanbieter-Komponenten, die nicht Auto-Encoding nutzen.
Vertieft pro Framework in xss-in-frameworks.
Schutz-Patterns
1. Sink-Vermeidung.
- Statt
innerHTMLliebertextContentoderinnerText. - Statt
evalundFunctionlieber strukturierte Logik. - Statt
setTimeout('code')liebersetTimeout(() => code).
2. URL-Schema-Validierung.
Wenn User-URLs in href oder location fließen, immer Schema prüfen. Nur http:, https:, mailto:, tel: u. ä. erlauben.
3. Source-Encoding.
Wenn Werte aus location.hash im DOM angezeigt werden, statt innerHTML per textContent rendern.
4. Sanitization vor innerHTML.
Wenn innerHTML unvermeidbar ist (Markdown-Output, Rich-Text-CMS), Sanitization-Library (DOMPurify) davorschalten. Vertieft in html-sanitization.
5. Content Security Policy.
CSP mit script-src ohne 'unsafe-inline' und ohne 'unsafe-eval' blockiert viele DOM-XSS-Folgen. Selbst wenn ein Sink injiziert wurde, kann das Skript nicht laufen, wenn CSP es nicht erlaubt. Vertieft in content-security-policy.
6. Trusted Types.
Modernste Browser-Antwort: alle DOM-Sinks akzeptieren nur explizit als sicher markierte Werte. Strukturelle Verhinderung von DOM-XSS. Vertieft in trusted-types.
7. Strikte Lint-Regeln.
ESLint-Plugins (eslint-plugin-security, eslint-plugin-no-unsanitized) warnen vor gefährlichen Sinks im Code. Im CI eingebaut, sieht man Probleme früh.
Erkennung in der Praxis
DOM-XSS ist deutlich schwerer zu finden als Server-XSS, weil:
- Server-Logs zeigen nichts — keine Spuren im Backend.
- WAFs blocken nichts — der Angriff erreicht das Backend nicht.
- Burp-/ZAP-Scanner finden DOM-XSS nur eingeschränkt.
Die wertvollsten Werkzeuge:
Burp DOM Invader. Burp Suite Pro hat seit 2022 das DOM Invader-Plugin — eine im Browser eingebaute Erweiterung, die DOM-Sources und -Sinks markiert und Test-Payloads injiziert. Praktisch das beste Werkzeug für DOM-XSS-Pentesting.
OWASP ZAP DOM XSS Scanner. Plugin „Web Sockets Filter" hat ein Modul für DOM-XSS. Weniger ausgereift als Burp DOM Invader, aber Open Source.
Statische Analyse.
- eslint-plugin-no-unsanitized — warnt vor
innerHTML,outerHTML,document.write. - Semgrep-Regeln für DOM-XSS-Patterns.
- CodeQL-Queries.
Manuelle Code-Reviews.
Suche im JS-Code nach:
innerHTML,outerHTML,insertAdjacentHTML.document.write,document.writeln.eval,Function,setTimeout('...').location.href = ...,location.assign(...)mit User-Input.
Pro Treffer fragen: kommt der Wert aus einer User-kontrollierten Source?
Häufige Stolperfallen
DOM-XSS in SPAs ist oft die einzige verbleibende XSS-Klasse
In modernen Framework-Apps (React, Vue, Angular) ist klassisches Server-XSS durch Auto-Encoding weitgehend gelöst. Was bleibt, sind DOM-XSS-Klassen — vor allem an Stellen, wo Framework-Auto-Encoding bewusst umgangen wird. Pentest-Reports finden hier am häufigsten Bugs.
Server-Side-Rendering schützt nicht vor DOM-XSS
Wer Next.js, Nuxt oder SvelteKit nutzt und denkt: „Mein SSR macht alles sicher" — falsch. SSR rendert den initialen HTML; jeder Folge-Client-Code (Hydration, Client-Routes, dynamische Updates) hat dieselben DOM-XSS-Risiken wie eine reine SPA.
URL-Fragmente und das Hash-Routing
Hash-Routing (#/page/...) ist eine klassische DOM-XSS-Falle. URL-Fragmente werden nicht vom Server gesehen — wenn der Client-Router den Hash unsanitisiert verarbeitet, hast du eine reine Client-Schwachstelle. Moderne Frameworks (React Router, Vue Router) handhaben das meist sicher, aber Custom-Router-Code muss geprüft werden.
postMessage ohne Origin-Check ist sehr verbreitet
Cross-Frame-Kommunikation per postMessage ist häufig — und der Default-Code überprüft oft den event.origin nicht. Jeder iframe, der in deine Seite eingebettet ist (Werbe-iframe, Drittanbieter-Widget), kann postMessage-Events senden. Origin-Check ist Pflicht. Vertieft in CSRF/iframe-Kapitel.
JSON.parse ist sicher, eval() nicht
Ein häufiges Anti-Pattern: eval('(' + jsonString + ')') zum Parsen. Niemals nötig — JSON.parse(jsonString) ist seit allen modernen Browsern verfügbar, sicher, und schneller. eval sollte aus dem Code verschwinden.
Self-XSS via Browser-Konsole
Manche Plattformen warnen Nutzer:innen beim Öffnen der DevTools-Konsole: „Stopp! Wenn dir jemand Code zum Pasten gegeben hat ... ". Das ist Anti-Social-Engineering, kein Anti-DOM-XSS. Trotzdem nützlich, weil Self-XSS-Scams (Bösewicht überredet Opfer, JS-Code in die Konsole zu pasten) ein realer Vektor sind.
Burp DOM Invader als Standard-Tool
DOM Invader (in Burp Pro) ist seit 2022 das beste Werkzeug für DOM-XSS-Tests. Es injiziert Canary-Strings in alle Sources, beobachtet, wo sie als JS, HTML oder URL interpretiert werden, und zeigt die Sinks im DevTools-Stack. Hat die Praxis von DOM-XSS-Hunting deutlich verbessert.
Weiterführende Ressourcen
Externe Quellen
- OWASP DOM-based XSS Prevention Cheat Sheet
- PortSwigger — DOM-based XSS
- Burp DOM Invader
- DOMGoat — DOM-XSS Lern-Lab
- DOM-XSS Wiki — Sources und Sinks Liste
- eslint-plugin-no-unsanitized
- MDN — postMessage