In den meisten Fällen ist die Antwort auf XSS einfach: Output-Encoding. User-Input wird beim Rendern HTML-encoded, <script> wird zu &lt;script&gt;. 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 &lt;b&gt;-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:

HTML bypass-beispiele.html
<!-- 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:

JavaScript dompurify-basic.js
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 entfernt

Konfiguration mit Whitelist:

JavaScript dompurify-config.js
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:

JavaScript dompurify-svg.js
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:

JavaScript dompurify-tt-integration.js
const clean = DOMPurify.sanitize(input, { RETURN_TRUSTED_TYPE: true });
element.innerHTML = clean;  // TT-konform, kein Error

Server-Side-Nutzung in Node.js:

DOMPurify funktioniert auch im Backend — mit jsdom als DOM-Implementierung:

JavaScript dompurify-node.js
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.

JavaScript sanitize-html-basic.js
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):

JavaScript browser-sanitizer-api.js
// 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 automatisch

Stand 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:

JavaScript minimal-whitelist.js
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?

JavaScript url-sanitization.js
// 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:

JavaScript dompurify-url.js
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 &lt;b&gt;-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

/ Weiter

Zurück zu XSS & Content Injection

Zur Übersicht