DOM Clobbering
Abuse named HTML elements to overwrite JavaScript variables and DOM properties — a script-less injection technique for bypassing sanitizers in CTF web challenges.
DOM Clobbering
DOM clobbering is a script-less injection technique. When you can inject
HTML but JavaScript is blocked — a strict CSP, an HTML sanitizer that strips
<script> and event handlers, or markdown that only allows benign tags — you
can still influence a page's JavaScript by abusing a legacy browser behaviour:
named HTML elements become properties on document and window.
If application code reads a global like window.config or document.x and you
can plant an element named config, you "clobber" that value with a reference
to your element. Turning a trusted variable into attacker-controlled data is
often enough to enable an open redirect, a script-src hijack, a sanitizer
bypass, or a full XSS downstream.
Where this matters
DOM clobbering shines in sanitized HTML contexts (comment fields, markdown renderers, rich-text) and behind CSPs that stop inline script. It needs no JavaScript of its own — it weaponizes the app's existing JavaScript.
The Core Behaviour
Browsers expose elements with an id or name attribute as named
properties on document (and on window for some). This is a backwards-compat
feature from the Netscape era that never went away.
<!-- Injected HTML -->
<a id="x">
<!-- Now in JavaScript: -->
<!-- document.x === the <a> element (not undefined) -->So a defensive check like this becomes bypassable:
// App code
if (!window.isAdmin) { restrict(); }<!-- Inject an element named isAdmin -> window.isAdmin is now truthy (an element) -->
<a id="isAdmin">The clobbered value is an HTMLElement, which is truthy. That alone defeats
many if (!x) guards and x || default fallbacks.
Clobbering Nested Properties (a.b)
Real code rarely reads a bare global; it reads config.url or window.app.token.
You can clobber two levels because a collection of same-named elements, or an
element containing a named child, resolves the inner property.
Two elements sharing an id produce an HTMLCollection; a child name
indexes into it:
<a id="config"></a>
<a id="config" name="url" href="https://evil.example"></a>
<!-- document.config.url -> the second <a> element -->From Clobbering to Impact
The technique is a primitive; the impact depends on what sink consumes the clobbered value.
Finding Clobberable Code
- Grep the client JS for reads of globals/DOM properties:
window.,document., bare identifiers used before assignment, and config lookups. - Look for
x || fallback,if (!x), anda.b.cchains sourced from globals — these survive clobbering and change behaviour. - Identify the sink:
.src,.href,location,innerHTML,eval,setTimeout, or a URL passed tofetch/import. - Check what HTML the injection point allows (which tags/attributes survive the
sanitizer) — you need
id/nameon permitted elements likea,form,input,img,iframe.
Defence
Sanitizers are not automatically safe
Many HTML sanitizers allow id and name by default,
so they do not stop clobbering. You must explicitly account for it.
- Don't trust DOM properties as variables. Initialize config with
const, read it from a non-clobberable source, and verify types (typeof x === 'string'). - Sanitizer config: strip or namespace
id/name, or use a library mode that prevents clobbering (DOMPurify'sSANITIZE_DOM/SANITIZE_NAMED_PROPS). - Trusted Types locks down dangerous sinks (
script.src,innerHTML) so a clobbered string can't reach them — the strongest structural defense. - Look up properties on objects you control, not on
document/window.
Checklist
- Confirm script is blocked but HTML injection with
id/nameis allowed - Map app JS for reads of globals /
document.*/a.bconfig chains - Clobber single property with
<a id="x">to defeatif(!x)/x||default - Nest with two same-
idanchors, orform+ namedinput, fora.b - Use an
<a href>(or DOMTokenList) when the sink needs a string - Drive the clobbered value into a sink:
.src,location,innerHTML,fetch - Chain with prototype pollution where useful
See Also
Last updated on