You didn't inject a script tag. You didn't modify any JavaScript. You didn't find a way to execute code directly.
You added an HTML element.
And somewhere in the application's JavaScript, a value that the developer assumed would be their own configuration object suddenly resolved to something else entirely.
No new code ran. The existing JavaScript behaved differently because the environment it ran in had changed.
That's the strange thing about DOM Clobbering. The browser isn't misbehaving. The HTML is valid. JavaScript is doing a perfectly normal property lookup. The problem is an assumption the developer made about what that lookup could return.
What "Clobbering" Actually Means
The DOM (Document Object Model) isn't just a tree of elements you manipulate with JavaScript. It participates in name resolution in ways that aren't always obvious.
Browsers expose certain HTML elements through named properties. When an element has an id, the browser may make it accessible through the window object's named property collection. When certain elements like forms or iframes use name attributes, similar behavior applies.
"Clobbering" describes what happens when attacker-controlled HTML introduces a name that collides with a property the application's JavaScript expects to control.
The application reaches for window.config expecting its own configuration object. But a DOM element with id="config" has been introduced. The lookup resolves to something the developer didn't put there.
DOM Clobbering isn't simply "every id becomes a global." The behavior is more nuanced than that. It depends on the element type, the property being accessed, which object is being queried, and specific browser named-property rules. But when the conditions align, an HTML element can change what a JavaScript name resolves to.
The Browser Behavior Behind It
Browsers expose what the HTML specification calls "named properties" on window and on certain DOM objects. This is a deliberate feature, not a bug. It exists for historical reasons and backward compatibility.
When you access window.someIdentifier, the browser doesn't only look at JavaScript variables you defined. It also checks whether any HTML element with a matching id exists on the page, among other named-property resolution rules.
window.config
Browser checks:
→ JavaScript variable named config?
→ Named property collision with a DOM element?
→ Something else in the named-property chain?
If an attacker can inject HTML into a page, they may be able to introduce an element whose id or name matches something the application's JavaScript relies on.
The browser then has two candidates for what that name means. Depending on the specifics, the DOM-introduced element may win.
Why id and name Matter Differently
Not all HTML attributes participate equally. Elements with id attributes can be reached through window named properties in many browsers, but the behavior isn't universal across all contexts. Certain elements have additional named-property behavior: forms and iframes are the most commonly relevant. A <form name="something"> creates a named property accessible through document. An iframe with a name attribute can be accessible through window.
This is also why the element type matters, not just the attribute. A <div id="config"> and a <form id="config"> both introduce the same name, but what you can do with the resulting named property differs.
A Concrete Example
Consider an application that initializes itself like this:
const config = window.config;
if (config && config.endpoint) {
fetch(config.endpoint + "/api/data");
}
The developer expects window.config to be a JavaScript object they defined elsewhere. It has an endpoint property pointing to their API.
Now suppose an attacker can inject HTML into the page before this code runs:
<div id="config"></div>
In browsers where id attributes participate in window named property resolution, window.config no longer necessarily returns the developer's JavaScript object. It may return the DOM element.
Application assumption:
window.config → { endpoint: "https://api.example.com" }
After HTML injection:
window.config → <div id="config"> element
config.endpoint → undefined (or something unexpected)
The fetch doesn't happen. Or it happens with an unexpected value. Depending on what the application does with this, the consequences range from a broken feature to something more significant.
The attacker never touched the JavaScript. They changed what the JavaScript operated on.
Forms, Iframes, and Nested Property Access
With a plain <div id="config">, window.config resolves to the element, and property access like config.endpoint returns undefined. Not always dangerous on its own.
But certain element types allow nested named properties. A <form> element named config with a child <input name="endpoint"> can make config.endpoint resolve to the input element rather than undefined. This happens because form elements expose their named child inputs as named properties.
<form id="config">
<input name="endpoint" value="...">
</form>
window.config → the form element
window.config.endpoint → the input element
This is why clobbering research often involves structured HTML rather than a single element. The goal is to satisfy the application's property-access pattern, not just the top-level lookup. This doesn't let an attacker construct arbitrary JavaScript objects, but it can produce enough structure to satisfy a conditional check and feed unexpected values into subsequent logic.
Why document.getElementById() Is Different
There's an important distinction between implicit named property resolution and explicit DOM queries.
// Implicit - subject to named property resolution
const config = window.config;
// Explicit - clearly queries the DOM
const el = document.getElementById("config");
These look similar but behave differently. document.getElementById("config") is an explicit API call. You get a DOM element, and you know you're getting a DOM element. The code is clearly asking for an element.
window.config is a named property lookup. The developer may intend it to resolve to a JavaScript object they defined. The lookup doesn't make that distinction for you.
DOM Clobbering exploits the gap between the developer's assumption and the browser's actual resolution behavior. Explicit APIs make the intent and the result clearer. They don't prevent you from making other mistakes, but they avoid one specific class of implicit name collision.
DOM Clobbering vs. XSS
These are different mechanisms and shouldn't be conflated.
XSS means attacker-controlled input becomes executable JavaScript. A script runs that the attacker introduced.
DOM Clobbering means attacker-controlled HTML changes the DOM environment in a way that influences existing JavaScript. The attacker's code doesn't execute. The application's own code executes against a different environment than it expected.
The attacker may not be able to run JavaScript at all. Perhaps the application has a strict Content Security Policy that blocks inline scripts and untrusted script sources. DOM Clobbering doesn't require script execution. It requires only the ability to introduce certain HTML.
This makes it a relevant technique in scenarios where traditional script injection is blocked but HTML injection is still possible.
How It Becomes a Security Problem
DOM Clobbering by itself isn't a vulnerability. It becomes one when the clobbered value influences something security-sensitive.
Attacker can influence page HTML
↓
Application relies on implicit named property resolution
↓
A security-sensitive lookup resolves to attacker-influenced value
↓
Application trusts the resolved value without type-checking
↓
Unexpected behavior
What matters is what the application does with the unexpected value: URL construction, configuration, resource loading, security checks, application initialization. In code paths that don't touch anything sensitive, the practical impact may be minimal. In code paths that do, the consequences scale with what the clobbered value controls.
Why Frameworks Don't Automatically Solve It
React, Vue, and similar frameworks run inside a browser. Browser semantics don't change because a framework is in use. If application code, or a library the application depends on, performs named property resolution on window or other browser-exposed objects, the underlying behavior still applies. Framework abstraction doesn't change how the browser resolves named properties.
Defenses
Avoid implicit named property resolution for security-sensitive values. Don't assume window.someIdentifier must refer to an application-defined object.
Keep configuration in explicitly controlled scope. A JavaScript module that exports a configuration object is not subject to named property collision in the same way a window-level reference is.
Validate type and structure, not just existence. Checking if (config) tells you the value is truthy. A DOM element is truthy. Verify that the value has the expected type and the properties you actually need before using it in sensitive operations.
Sanitize attacker-controlled HTML. If the application accepts user-supplied HTML, an appropriate sanitizer can strip id and name attributes that could participate in clobbering.
Prefer explicit DOM queries when querying the DOM. document.getElementById("config") clearly asks for an element and returns one. window.config makes an implicit assumption about what that name resolves to.
CSP and Trusted Types address related script-injection risks but don't fix the core issue: an unsafe assumption about named property resolution. The fix is removing that assumption and controlling attacker-influenced HTML at the source.
The Deeper Lesson
Nothing in a DOM Clobbering scenario is technically broken.
The browser exposes named properties exactly as specified. The HTML is valid markup. The DOM is constructed the way browsers construct DOMs. JavaScript performs a normal property lookup and gets back a value.
The problem is a developer assumption: that the lookup can only return what the developer put there.
That assumption is wrong. The DOM is a shared environment. HTML that an attacker introduces participates in that environment. Browser name resolution doesn't distinguish between developer-controlled and attacker-controlled elements.
HTML is not just markup.
HTML creates the DOM.
The DOM participates in browser name resolution.
JavaScript operates in that environment.
When application code assumes a name belongs entirely to the application's own JavaScript, it's assuming something the browser doesn't guarantee.
The attacker didn't change the JavaScript. They changed the environment the JavaScript was running in. The JavaScript did exactly what it was written to do.
Top comments (0)