Mutation XSS (mXSS): When Safe HTML Turns Dangerous After Parse
Mutation XSS is the nasty cousin of ordinary HTML injection. You submit markup that looks harmless to a sanitizer. The browser parses it, mutates the tree, serializes it again, and suddenly a <script> or event handler exists that the sanitizer never approved.
The bug is not "filter bypass" in the slogan sense. It is a disagreement between two HTML parsers or between parse and serialize. Once you see that framing, the weird payloads stop looking random and start looking like deliberate parser stress tests.
Mental model
Round-trip looks like:
- Attacker string → sanitizer (parser A) → "safe" HTML
- Safe HTML → browser (parser B) → live DOM
- Sometimes: DOM →
innerHTMLreadback → string that differs from step 1
If step 3 (or a second write of step 2) introduces executable markup, you have mXSS. Older bugs abused namespace nesting (math, svg, table truncation) so that a sanitized tree reparented nodes into a script context.
You will not find these with a single <script>alert(1)</script> probe. You need exotic trees.
Hunting approach that wastes less time
Identify client or server sanitizers: DOMPurify version, OWASP Java HTML Sanitizer, custom regex, CMS "safe HTML" fields. Note the version; mXSS history is full of version-specific fixes.
Work from public research patterns, then adapt—do not paste someone else's full exploit as your only content. Try nested broken tags, <noscript> interactions in quirks contexts, SVG/MathML mixtures, and forms that rewrite DOM on paste.
Instrument the page: breakpoint Element.innerHTML set, log the string before and after. If the string grows a <script> after assignment, pause and dump the input that caused it.
I've watched teams "fix" mXSS by blacklisting the word script while leaving the mutation path intact. The next mutation just uses <img onerror>.
Proof standards
Provide the exact input, the sanitizer output (as returned by the API), and the DOM after insertion (copy outerHTML). Two screenshots help: network response of sanitized HTML, and DOM panel after layout. Keep payloads minimal; a huge fuzz crash dump is not a report.
Severity follows where the HTML lands—stored profile bio with mXSS is stored XSS. Reflective preview widgets still matter if an attacker can lure a victim to the preview URL.
If the mutation only appears after a second innerHTML write—copy, undo, or "beautify HTML" features—include that interaction. Reviewers will not guess a hidden UI path.
Fixes that stick
Upgrade sanitizers, prefer well-tested libraries over home-grown parsers, avoid double-parsing across server and client with different engines, and set a tight CSP as a backstop. CSP will not make mXSS "safe" if you allow 'unsafe-inline', but a nonce-based policy limits fallout while you patch.
Pin sanitizer versions in lockfiles and add regression tests that replay your mutation sample. One fixed release means little if a dependency float brings an old parser back.
When you file these, teach triage how the mutation happens in one short paragraph. The chain is weird; clarity sells severity.