Lion Cipher Lab
Defeat a sanitizer with mutation XSS (mXSS), then fix it
- AppSec / DevSecOps
- Advanced
- ~75 min
- Free · no account required
Why it matters
Most teams treat an HTML sanitizer as a magic wand: run untrusted HTML through it, get back 'clean' HTML, drop that into the page, done. Mutation XSS is the reason that mental model quietly fails. A sanitizer works by PARSING the input into a DOM, walking that DOM, and stripping anything dangerous (script tags, on* event handlers, javascript: URLs). It then hands you back a STRING — and here is the trap: the browser has to PARSE THAT STRING AGAIN when you assign it to innerHTML. The sanitizer certified the first parse; the browser renders the second. The browser's HTML parser is not a pure…
What you'll learn
- defining mutation XSS (mXSS) precisely: markup a sanitizer certifies as CLEAN in the parsed DOM, that becomes a LIVE payload after innerHTML re-serialises and re-parses it
- naming the three moving parts — sanitizer (inspects a parsed DOM and strips danger), serialization (turning a DOM back into an HTML string), and the round-trip (parse -> inspect -> serialize -> parse again)
- seeing that a sanitizer's verdict is bound to PARSE #1, while the browser renders PARSE #2 of the serialized string, and that the two parses can disagree
- explaining WHY the parser mutates markup on the round-trip: namespace confusion at SVG/MathML/foreignObject integration points, plus attribute and entity re-interpretation
- reproducing a documented real-world sanitizer bypass by pinning a known-vulnerable sanitizer version (DOMPurify < 2.0.17) and watching a certified-clean string fire alert()
- measuring the exploit correctly: the sanitizer returns a string it declares safe, yet assigning that same string to innerHTML executes attacker JavaScript
- fixing it three ways: (a) never round-trip sanitised HTML through innerHTML again — insert the sanitizer's DOM nodes directly (RETURN_DOM_FRAGMENT), (b) run a MAINTAINED sanitizer at a patched version wired to Trusted Types (RETURN_TRUSTED_TYPE), (c) layer a strict CSP
- the judgment: sanitising is an arms race against browser parser quirks, so the durable plan is to not inject HTML at all (textContent / auto-escaping templates), with a patched sanitizer + CSP as the fallback, never the whole plan
Steps in this lab
- Mutation XSS in one breath: the round-trip that betrays the sanitizer
- Build a naive sanitizer that trusts the round-trip
- See the round-trip mutate: CLEAN verdict, different rendered DOM
- Defeat a REAL sanitizer: pin a known-vulnerable DOMPurify and fire alert()
- Fix it: patch the sanitizer, stop the round-trip, add depth
- The judgment: sanitising is the fallback, not the plan