Athena Wiki

DOM Clobbering

webintermediate

Abuse named HTML elements to overwrite JavaScript variables and DOM properties — a script-less injection technique for bypassing sanitizers in CTF web challenges.

webxssdom-clobberinghtml-injectionsanitizer-bypasssanitizationtrusted-types

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

  1. Grep the client JS for reads of globals/DOM properties: window., document., bare identifiers used before assignment, and config lookups.
  2. Look for x || fallback, if (!x), and a.b.c chains sourced from globals — these survive clobbering and change behaviour.
  3. Identify the sink: .src, .href, location, innerHTML, eval, setTimeout, or a URL passed to fetch/import.
  4. Check what HTML the injection point allows (which tags/attributes survive the sanitizer) — you need id/name on permitted elements like a, 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's SANITIZE_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/name is allowed
  • Map app JS for reads of globals / document.* / a.b config chains
  • Clobber single property with <a id="x"> to defeat if(!x) / x||default
  • Nest with two same-id anchors, or form + named input, for a.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

On this page