In den meisten Fällen ist die Antwort auf XSS einfach: Output-Encoding. User-Input wird beim Rendern HTML-encoded, <script> wird zu <script>. Manchmal aber brauchst du echtes HTML — User-erzeugte Rich-Text-Inhalte mit Formatierung, Markdown-Output, CMS-Artikel, Mail-Inhalte. Dann reicht Encoding nicht; du brauchst Sanitization. Dieser Artikel zeigt, wie Sanitization technisch funktioniert, welche Library was leistet — und warum jeder eigene Regex-Sanitizer scheitert.
Wann Sanitization nötig ist
In den meisten Web-Apps ist die richtige Antwort auf User-Input: kein Roh-HTML rendern, nur encoded-Text. Damit gibt es kein XSS — was nicht als HTML interpretiert wird, kann auch kein Skript ausführen.
Aber: manche Use-Cases verlangen Roh-HTML:
- Rich-Text-Editoren (CMS, Forum-Beiträge mit Formatierung) speichern HTML.
- Markdown-Renderer wandeln Markdown in HTML um — und manche Markdown-Dialekte erlauben Roh-HTML-Tags.
- E-Mail-Inhalte in Webmail-Apps werden als HTML angezeigt.
- Whitelist-spezifische Tags in Kommentaren (
<a>,<b>,<i>erlauben, alles andere blockieren). - AI-generierte HTML-Outputs, die in der App angezeigt werden.
- Externe Inhalte (RSS-Feeds, Embed-Snippets), die als HTML kommen.
In diesen Fällen ist Encoding kein Werkzeug — du willst ja, dass <b> als Fett-Tag rendert, nicht als <b>-Text. Stattdessen brauchst du Sanitization: das HTML wird parsiert, gefährliche Teile (Skript-Tags, Event-Handler, javascript:-URLs) werden entfernt, sicheres HTML wird zurückgegeben.
Warum eigene Regex-Sanitizer scheitern
Ein verbreiteter Anti-Pattern: „Wir filtern <script>-Tags mit Regex, fertig." Funktioniert nicht. Drei strukturelle Probleme:
1. HTML ist nicht regulär.
HTML hat Verschachtelung, Attribute, mehrere Kontexte, Entity-Encoding. Eine Sprache, die nicht durch reguläre Grammatik beschrieben werden kann. Regex sind dafür strukturell ungeeignet.
2. Filter-Bypass-Kreativität.
Selbst wenn du <script> filterst, gibt es Hunderte alternative XSS-Vektoren:
<!-- Variationen mit Mixed-Case und Whitespace -->
<ScRiPt>alert(1)</ScRiPt>
<script
>alert(1)</script>
< script>alert(1)< /script>
<!-- Inline-Event-Handler -->
<img src=x onerror=alert(1)>
<body onload=alert(1)>
<svg onload=alert(1)>
<!-- javascript:-URL -->
<a href="javascript:alert(1)">Click</a>
<!-- iframe-Tricks -->
<iframe srcdoc="<script>alert(1)</script>">
<iframe src="data:text/html,<script>alert(1)</script>">
<!-- SVG mit eingebettetem Code -->
<svg><script>alert(1)</script></svg>
<!-- HTML5-Features -->
<video><source onerror=alert(1)>
<audio src=x onerror=alert(1)>
<!-- ... und Hunderte weitere -->Eine vollständige Liste füllt eigene Bücher. html5sec.org und PayloadsAllTheThings XSS-Liste dokumentieren die Variations-Breite.
3. Mutated XSS (mXSS).
Eine besonders fiese Klasse: dein Sanitizer sieht den HTML-Input wie der Browser-Parser — aber subtil anders. Eingabe wirkt harmlos für den Sanitizer, wird aber vom Browser anders gerendert.
Mario Heiderich hat 2014 die Klasse systematisch erforscht. Beispiel: <noscript><p title="</noscript><img src=x onerror=alert(1)>"></p> — manche Sanitizer parsen das als ungefährliches Markup, der Browser rendert es als XSS.
Konsequenz: Sanitization braucht eine vollwertige HTML-Parser-Implementierung — und das ist Library-Aufgabe, nicht Regex-Aufgabe.
DOMPurify — der Industrie-Standard
DOMPurify ist die meistgenutzte HTML-Sanitization-Library im Web. Entwickelt von Mario Heiderich und Team bei Cure53, gepflegt seit 2014. Wird von GitHub, Google, Microsoft, Mozilla, vielen großen Anwendungen produktiv eingesetzt.
Grundnutzung:
import DOMPurify from 'dompurify';
const dirty = '<img src=x onerror=alert(1)><b>Hello</b>';
const clean = DOMPurify.sanitize(dirty);
// Ergibt: '<img src="x"><b>Hello</b>'
// onerror-Attribut wurde entferntKonfiguration mit Whitelist:
const clean = DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
ALLOWED_ATTR: ['href', 'title'],
ALLOWED_URI_REGEXP: /^(https?:|mailto:|#)/i,
});SVG-Modus:
DOMPurify kann auch SVG sanitisieren — wichtig, wenn deine Anwendung SVG-Uploads erlaubt:
const cleanSvg = DOMPurify.sanitize(uploadedSvg, {
USE_PROFILES: { svg: true, svgFilters: true },
});Trusted-Types-Integration:
Wenn deine App Trusted Types aktiv hat, gibt DOMPurify direkt TrustedHTML-Objekte zurück:
const clean = DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: true });
element.innerHTML = clean; // TT-konform, kein ErrorServer-Side-Nutzung in Node.js:
DOMPurify funktioniert auch im Backend — mit jsdom als DOM-Implementierung:
import { JSDOM } from 'jsdom';
import createDOMPurify from 'dompurify';
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
const clean = DOMPurify.sanitize(userInput);sanitize-html — die Node.js-Alternative
sanitize-html ist eine reine Node.js-Library, ohne DOM-Abhängigkeit. Beliebt im Backend, weil leichtgewichtig.
import sanitizeHtml from 'sanitize-html';
const clean = sanitizeHtml(dirty, {
allowedTags: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
allowedAttributes: { 'a': ['href'] },
allowedSchemes: ['http', 'https', 'mailto'],
});Wann sanitize-html, wann DOMPurify+jsdom?
- sanitize-html — schneller, leichtgewichtiger. Gut für Server-Side-Rendering, API-Filterung, Mail-Verarbeitung.
- DOMPurify+jsdom — kennt die exakten Browser-Parser-Quirks (inkl. mXSS-Schutz). Schwerer, aber näher an Browser-Verhalten.
Wenn deine Anwendung sowohl Browser- als auch Server-Sanitization braucht, kannst du DOMPurify im Browser + sanitize-html im Server kombinieren — beide haben kompatible Konfigurations-Strukturen.
Die kommende Browser-Sanitizer-API
W3C arbeitet an einer eingebauten Sanitizer-API — Browser liefert die Sanitization out of the box. Vorteile:
- Keine Library-Dependency nötig.
- Maximal aktuell — Browser-Hersteller kennen die HTML-Parser-Quirks am besten.
- Performant — native Implementierung.
API-Beispiel (Stand 2026 in Chrome):
// Stand 2026: in Chrome experimentell, in anderen Browsern unterwegs
const sanitizer = new Sanitizer();
const clean = sanitizer.sanitizeFor('div', dirtyHtml);
// Oder mit Element-Methode
element.setHTML(dirtyHtml); // sanitisiert automatischStand 2026:
- Chrome / Edge — schrittweise Implementierung, hinter Flag.
- Firefox — Vorschau-Implementierung.
- Safari — angekündigt, in Arbeit.
Solange die API nicht breit verfügbar ist, bleibt DOMPurify die De-facto-Standard-Lösung. Wer auf zukünftige Browser-API umstellen will, kann sie als Drop-In behandeln — die Konfigurations-Patterns sind ähnlich.
Sanitization-Strategien
Whitelist vs. Blacklist.
Sanitization-Libraries arbeiten immer mit Whitelists. Du sagst: „Diese Tags und Attribute sind erlaubt, alle anderen werden entfernt." Niemals mit Blacklist („alle Tags außer <script> sind erlaubt") — die ist strukturell brüchig, weil HTML zu vielfältig ist.
Minimal-Whitelist als Default.
Für Kommentare und kurze User-Texte reichen oft wenige Tags:
const clean = DOMPurify.sanitize(input, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'br', 'p'],
ALLOWED_ATTR: ['href'],
});Markdown-Output-Whitelist.
Wenn Markdown gerendert wird, ist die Whitelist breiter — <ul>, <ol>, <li>, <h1>–<h6>, <blockquote>, <code>, <pre>. DOMPurify hat dafür USE_PROFILES: { html: true }.
Rich-Text-Editor-Whitelist.
CMS und Forum-Editoren (TinyMCE, CKEditor, ProseMirror) erlauben mehr — <img> mit src/alt, <table> mit Spalten, Stil-Attribute. Vorsicht bei <img> mit onerror — Editor und Sanitizer müssen zusammenpassen.
Profile-basierte Sanitization.
Manche Libraries bieten Profile (DOMPurify: MathML, SVG, html, usePropertyMode). Für spezialisierte Anwendungen lohnt das Studium der Library-Doku.
URL-Sanitization
Ein häufiges Sub-Problem: User gibt einen Link ein (<a href="...">). Was, wenn der Link javascript:-Schema hat?
// Schadhaft: nicht sanitisiert
link.href = userUrl; // userUrl könnte "javascript:alert(1)" sein
// Sicher: Schema-Validierung
function safeUrl(input) {
try {
const url = new URL(input, location.origin);
const ALLOWED = new Set(['http:', 'https:', 'mailto:', 'tel:']);
return ALLOWED.has(url.protocol) ? url.href : '#';
} catch {
return '#';
}
}
link.href = safeUrl(userUrl);DOMPurify behandelt das mit ALLOWED_URI_REGEXP:
DOMPurify.sanitize(input, {
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):|#)/i,
});URLs sind eine eigene Sanitization-Klasse — sie müssen separat von HTML-Sanitization behandelt werden. Wer <a href="..."> rendert, prüft das Schema explizit.
Anti-Patterns
Anti-Pattern 1 — strip_tags() oder striptags() als Sanitization.
PHP und manche Node-Libraries haben strip_tags-Funktionen, die einfach <...>-Sequenzen entfernen. Das ist keine Sanitization — Attribute, Schema-Manipulationen, Entity-Encoding-Tricks werden nicht behandelt. Niemals als XSS-Schutz nutzen.
Anti-Pattern 2 — htmlspecialchars() für Roh-HTML-Output.
Wenn du Roh-HTML rendern willst, ist Encoding falsch — <b> wird zu <b>-Text. Wenn du keine Roh-HTML willst, ist Encoding korrekt. Wer das vermischt, hat entweder XSS oder defektes Rendering.
Anti-Pattern 3 — Sanitization einmal, mehrfach Rendering.
Wenn du Inhalt einmal sanitisierst und in DB speicherst — und der Sanitizer später aktualisiert wird mit besseren Patterns —, hast du alte Daten mit alter Sanitization-Logik. Besser: Roh-Input speichern, bei jedem Render sanitisieren. Sanitizer-Updates wirken so rückwirkend.
Anti-Pattern 4 — Sanitization für Schema-Validierung.
Sanitization löst nicht „diese Eingabe muss eine Mail-Adresse sein" — das ist Schema-Validierung. Beide Schichten getrennt halten: Schema-Validation für Struktur, Sanitization für HTML-Roh-Insertion.
Anti-Pattern 5 — Sanitization als einzige XSS-Verteidigung.
Sanitization ist eine Schicht. Defense-in-Depth heißt: zusätzlich CSP, Trusted Types, HttpOnly-Cookies. Wenn dein Sanitizer einen Bypass hat (kommt vor), schützen die anderen Schichten.
Interessantes
Mario Heiderich und das Cure53-Team
DOMPurify wird seit 2014 von Mario Heiderich (Cure53) und Team gepflegt. Heiderich ist auch der/die Urheber:in der mXSS-Forschung — viele der subtilen Browser-Parser-Quirks, die normale Sanitizer übersehen, sind in DOMPurify dokumentiert und gehandhabt.
Performance: DOMPurify ist überraschend schnell
Pro Sanitization-Aufruf liegt der Overhead bei wenigen Millisekunden für typische HTML-Größen. Vergleich zu Regex-„Sanitizern": DOMPurify ist etwas langsamer pro Aufruf, aber sicher. Wer Performance optimieren will, sollte Server-side cachen, nicht den Sanitizer umgehen.
DOMPurify und Trusted Types: Native Unterstützung
Seit DOMPurify 2.0 gibt es die Option RETURN_TRUSTED_TYPE: true. Damit erzeugt jeder Sanitization-Aufruf direkt ein TrustedHTML-Objekt — keine separate Policy nötig. Sehr smooth für TT-Migrationen.
sanitize-html ist ApostropheCMS-Maintainer
sanitize-html wird vom Team hinter ApostropheCMS gepflegt — gefördert durch produktiven Einsatz im CMS. Solide Reife. Etwas weniger Edge-Case-Tiefe als DOMPurify (kein mXSS-Schutz auf Browser-Parser-Niveau), aber für Backend-Sanitization oft ausreichend.
Bleach für Python
Wer in Python sanitisiert: Bleach ist die Mozilla-Library. Funktional analog zu sanitize-html, gut gepflegt. Für Django- und Flask-Apps geeignet.
OWASP Java HTML Sanitizer
Für Java-Backends: OWASP Java HTML Sanitizer ist die etablierte Library. Fluent-API für Whitelist-Konfiguration, Trusted-Types-kompatible Output-Formen.
Sanitizer-API: Standard noch nicht final
Die W3C Sanitizer-API ist seit 2019 in Entwicklung. Chrome hat eine Implementierung hinter Flag (--enable-experimental-web-platform-features). Spec-Diskussion 2024/25 noch nicht abgeschlossen. Bis dahin bleibt DOMPurify die produktive Wahl.
Weiterführende Ressourcen
Externe Quellen
- DOMPurify GitHub
- sanitize-html (Node.js)
- Bleach (Python)
- OWASP Java HTML Sanitizer
- W3C Sanitizer API Spec
- OWASP XSS Prevention Cheat Sheet
- HTML5 Security Cheatsheet
- Cure53 — Forschung zu mXSS und Sanitizer-Bypass