TL;DR / DOM clobbering exploits the browser's legacy behavior of exposing named HTML elements as properties on
documentandwindow, allowing attackers to override JavaScript globals and configuration objects with DOM nodes.
How It Works
┌────────┐
│ HTML │
└────────┘
│
<img name="config"> │
<form name="config"> │
<input name="url"> │
│
│
↓
┌────────────────────┐
│ document.config │
│ = <img> element! │
└────────────────────┘
│
│
│
│
↓
┌────────────────┐
│ JS expects │ config.url
│ object, gets │ = attacker value
│ DOM node │
└────────────────┘
DOM clobbering is an attack vector that is frequently overlooked because it does not involve script injection in the traditional sense. It exploits a decades-old browser feature: named access on the document and window objects. When an HTML element has a name or id attribute, the browser automatically makes it accessible as a property of document (for embeds, forms, images, objects with name) or window (for elements with id).
The Named Access Mechanism
The HTML spec defines "named access on the Window object": any element with an id becomes a property of window. Additionally, certain elements (embed, form, image, object) with a name attribute become properties of document. If your JavaScript checks for window.config or document.config and that property does not exist, an attacker who can inject HTML (even without script injection) can create a named element that satisfies the property lookup.
Consider code like: const endpoint = window.config || '/api/default'. An attacker injects <img id="config">. Now window.config resolves to the <img> element instead of undefined, and the fallback is never reached. The string coercion of a DOM element produces unexpected results, potentially breaking logic or enabling further exploitation.
Two-Level Clobbering
The attack becomes more powerful with forms. A <form name="config"> creates document.config pointing to the form element. Named inputs within that form become properties of the form element itself. So <form name="config"><input name="url" value="https://evil.com"> makes document.config.url return the input element, whose toString() in some contexts returns its value. This enables two-level property access clobbering, which can override nested configuration objects.
Where It Becomes Dangerous
DOM clobbering is primarily dangerous in contexts where HTML injection is possible but script injection is blocked -- for example, by a strict CSP or an HTML sanitizer that strips <script> tags and event handlers but allows benign elements like <img>, <form>, and <input>. The attacker cannot run JavaScript directly, but they can influence JavaScript execution by clobbering global variables.
Common exploitation targets include: configuration objects checked with window.CONFIG || defaults, feature flags accessed via global properties, library initialization code that checks for existing globals, and URL construction logic that reads from named elements.
Real-World Impact
DOM clobbering has been used to bypass DOMPurify in specific versions, where the sanitizer's internal variables were clobbered by injected HTML. It has enabled script execution through indirect means -- clobbering a URL property that is later used in a script src attribute or passed to fetch(). In security-critical applications, it can override CSP nonce storage if nonces are cached in a global variable.
Defense Strategies
The primary defense is to never rely on named access. Always use explicit DOM queries (document.getElementById(), document.querySelector()) rather than bare property access on window or document. Declare variables with const/let at the module level -- block-scoped variables cannot be clobbered by DOM elements because they take precedence in the scope chain.
For HTML sanitizers, consider using an allowlist that strips id and name attributes from user-provided HTML, or at minimum validate them against a set of known-safe values. The Object.freeze() function can protect critical configuration objects from being overridden if they are defined before any user HTML is parsed.
CSP's require-trusted-types-for 'script' provides indirect protection by ensuring that even if a clobbered value reaches a dangerous sink, it must pass through a Trusted Types policy first.
Gotchas
- HTML sanitizers that allow
idandnameattributes are vulnerable. DOMPurify allows them by default. Use theSANITIZE_NAMED_PROPSoption to prefix them withuser-content-. typeof window.x !== 'undefined'is defeated by clobbering. A DOM element is truthy and has type'object', passing most existence checks.- ES module scope protects against clobbering because module-level variables shadow
windowproperties. Global scripts are more vulnerable. <a id="x" href="javascript:alert(1)">allowswindow.x.toString()to return the href value, potentially enabling script execution in contexts that coerce the clobbered value to a string and use it as a URL.- Multiple elements with the same
idcreate anHTMLCollection, which behaves differently from a single element and can break code in unexpected ways.