DOM Clobbering
detect, understand, remediate
DOM clobbering shadows global properties on window and document by injecting HTML elements with controlled id and name attributes. The technique bypasses HTML sanitisers and Content Security Policy because no script tag or event handler is required: page JavaScript reads what it thinks is configuration and instead reads attacker-controlled DOM references.
No credit card required. Free plan available forever.
What is DOM clobbering?
DOM clobbering is a client-side technique in which an attacker injects benign-looking HTML elements with controlled id or name attributes into a page, and the browser silently exposes those elements as named global properties on window and document. JavaScript already running on the page reads what it thinks is a configuration object, a callback, or a URL, and instead reads an attacker-controlled DOM reference. The script keeps executing without throwing, and the attacker has redirected its behaviour without ever delivering a script tag.
The behaviour is not a browser bug. The HTML and WHATWG specifications define named access on the Window object and document properties named after element id and name attributes. DOM clobbering exploits the gap between what the spec exposes and what application code expects: developers reading window.config or document.cookie assume a JavaScript-defined value, but a markup injection that introduces <img name="config"> earlier in the document tree wins the lookup.
DOM clobbering is tracked under CWE-79 when the chain culminates in script execution and CWE-1321 adjacent patterns when the attack mutates JavaScript object state through the DOM. Like cross-site scripting and prototype pollution, DOM clobbering rarely produces an exploit on its own. Its leverage is as a first stage that bypasses HTML sanitisers and Content Security Policy: the markup that lands on the page contains no script, the sanitiser sees only an anchor or a form element, and the second-stage JavaScript reads attacker-controlled data through the global namespace.
How it works
Find a markup injection sink
The attacker locates a place where user-controlled HTML is rendered with a sanitiser that allows tag attributes such as id and name. Common sinks include comment fields, profile bios, support replies, and any rich-text editor that ships markup downstream rather than a structured AST.
Identify a global lookup
The attacker reads the page JavaScript and finds a property access that the application treats as configuration, a callback, or a URL. Patterns that look like window.CONFIG, document.scripts, or test ?? defaultValue with implicit globals are candidates.
Inject a clobbering element
The attacker submits HTML such as <a id="CONFIG" href="https://attacker.example"></a> or <form id="CONFIG"><input name="endpoint" value="..."></form>. The DOM exposes window.CONFIG as the anchor element; the application reads .href and proceeds as though it had loaded a JSON object.
Cash the payload
The downstream code uses the clobbered value: a script src is set from the anchor href, a fetch call is issued to an attacker URL, a redirect target is overridden, or a sanitiser configuration is replaced. The CSP allows the request because the source looks first-party; the sanitiser passed because no script tag was injected.
Common clobbering payloads
The payloads below are the primitives that show up in real engagements. Each one returns a value the application reads through the global namespace, with no script tag and no event handler. A scanner or tester that only looks for <script> or onerror= will miss every entry on this list.
| Pattern | Example | What it returns |
|---|---|---|
| Anchor href as a string | <a id="target" href="//evil"> | window.target.toString() coerces to the URL string. Code that reads window.target ?? defaultUrl into a script src or a fetch URL is now attacker-controlled. |
| Form with named input | <form id="config"><input name="url" value="//evil"></form> | window.config.url returns the input element; .value reads the attacker string. Forms are the most flexible primitive because they namespace clobbered properties. |
| Two elements, same id | <a id="x"><a id="x"> | window.x becomes an HTMLCollection with .length and indexed access. Code that branches on truthy window.x or that reads window.x.length sees attacker-controlled values. |
| Image name on document | <img name="cookie"> | document.cookie can be shadowed in older browsers and selected APIs. Modern browsers protect document.cookie specifically, but adjacent properties (document.scripts, document.images) remain reachable. |
| Iframe with name | <iframe name="loader" src="//evil"> | window.loader returns the iframe Window; postMessage targets are clobbered. A script that postMessages to window.loader leaks data to the attacker frame. |
| Nested object property | <form id="a"><input name="b" value="c"></form> | window.a.b returns the input element; .value returns the string. Two-level lookups give the attacker structured payload delivery without script. |
Common causes
Allowing id and name on user-rendered HTML
Sanitisers configured to permit anchor, form, and image tags often allow id and name attributes by default. The attributes look harmless on their own, and the sanitiser test suite focuses on script execution rather than DOM exposure. Every allowed id is a potential clobbering primitive.
Reading globals as a configuration source
Application code that pulls window.CONFIG, window.endpoints, or window.featureFlags trusts the global namespace as a single source of truth. Any markup injection that lands on the same document can shadow those names. The fix is keeping configuration on a closure, a module export, or a structured data attribute, not on window.
Implicit fallbacks against missing globals
Patterns like config = window.CONFIG || defaults assume CONFIG is either an object or undefined. A clobbered HTMLAnchorElement is truthy and stringifies to a URL, so the fallback never runs and the application takes the attacker path silently.
Trusting CSP to block the chain
Strict CSP blocks inline script and disallows new script sources, which prevents many XSS chains. DOM clobbering does not need a script tag; the markup is plain HTML the CSP allows. CSP is necessary but not sufficient against this class.
Rendering markup before serialisation review
Server-side rendering paths that emit pre-rendered HTML to the client can include user input where the same string was sanitised once at write time, but the rendered version is later mutated by client code that re-introduces unsafe attributes. Each render path needs its own attribute allowlist review.
Outdated sanitiser libraries
DOMPurify, sanitize-html, and similar libraries have shipped specific clobbering protections in recent versions. Applications pinned to old releases miss the SANITIZE_NAMED_PROPS option in DOMPurify and similar guards. Dependency drift here lands the entire class of vulnerability.
How to detect it
Automated detection
- SecPortal's authenticated scanner probes user-controlled HTML sinks with markup payloads that include id and name attributes targeting common configuration globals, then verifies whether the attribute survives sanitisation and is reachable through window or document.
- The code scanner flags JavaScript patterns that read globals into security-relevant sinks: window.config or window.endpoints assigned into script src, fetch URL, redirect target, or sanitiser configuration. The static rule does not need to know the exact clobber payload to catch the pattern.
- Findings are recorded with the original markup, the surviving sanitised output, the global lookup that read it, and the downstream sink. The full chain is on the finding so the verification step does not need to reconstruct it from scratch.
Manual testing
- Open the rendered page in DevTools and run
Object.keys(window)filtered for short, common names. Anything that resolves to an HTMLElement rather than the expected JavaScript value is a clobbering primitive already in scope. - Inject test markup such as
<a id="PROBE" href="//probe">through every sink the application accepts. Confirm in DevTools whether window.PROBE returns the anchor element. If it does, every property accessor downstream is a candidate sink. - Walk the application JavaScript for property reads against window, document, and self. Each read is a potential target. Pair the read with a markup sink that reaches the same document and you have a chain candidate.
- Test PortSwigger's published lab payloads against your sanitiser configuration to confirm whether the form, anchor, and iframe primitives survive. Sanitisers tested only against XSS payloads routinely pass clobbering payloads.
How to fix it
Strip id and name attributes from user-rendered HTML
The cheapest fix is the most direct: do not allow user-controlled markup to ship attributes that the browser exposes through named access. DOMPurify supports the SANITIZE_NAMED_PROPS option for this; sanitize-html requires explicit allowedAttributes configuration. The default of permitting these attributes for accessibility is the wrong default for user content.
Read configuration from a structured source, not from window
Move config values, endpoints, feature flags, and callbacks off the window object and onto a closure-scoped object or a JSON script tag with a unique id that the sanitiser strips. Code that reads window.CONFIG is permanently exposed; code that reads cfg from a closure is not.
Use Object.hasOwn or typeof checks before truthy fallbacks
The pattern config = window.CONFIG || defaults takes the attacker path silently when the global is a clobbered element. Replace with an explicit type check: typeof window.CONFIG === 'object' && window.CONFIG !== null. The check fails on HTMLElement and the fallback runs. Same idea for arrays: Array.isArray(window.list) before iterating.
Apply a strict Content Security Policy in concert
CSP cannot block DOM clobbering on its own, but it raises the cost of cashing the chain. A strict CSP that uses nonce-based script policies and forbids inline event handlers means the attacker has to chain DOM clobbering with another primitive (a redirect, a fetch overwrite) rather than escalating directly to script. The chain still exists; it costs more.
Pin a sanitiser version that handles named-property clobbering
DOMPurify 2.4 and later, sanitize-html 2.7 and later, and Trusted Types policies in modern browsers each address parts of the class. Pin to a known-good version, exercise the named-property test cases in CI, and add a regression test the next time the sanitiser is upgraded.
Treat sanitiser configuration as security-critical code
A sanitiser pass list (allowed tags, allowed attributes, schemes) is configuration that lands user markup directly into a security boundary. Review changes the same way you review authentication code: pull request, second reviewer, regression suite. Sanitisers that grow attribute allowances over time accumulate clobbering primitives by default.
Where DOM clobbering shows up in real engagements
DOM clobbering rarely lands on a report as a standalone finding. It is almost always a contributing primitive that bypassed a sanitiser, defeated a CSP, or made an XSS chain reachable in an environment that otherwise blocked direct script injection. The chains below are common ways the primitive turns into an exploitable issue worth recording on a client report.
Sanitiser bypass into stored XSS
A platform sanitises user comments with a permissive ruleset that allows id attributes for accessibility. An attacker plants an anchor whose id matches a window-scoped library callback. The library reads window.callback expecting a function, calls .toString on it, and writes the result to innerHTML. The string contains an attacker-supplied URL that the library treats as trusted markup.
Defeating CSP nonce policy
The application uses nonce-based CSP for script tags but reads the nonce from window.NONCE in a helper that injects late-loaded scripts. A clobbered element shadows window.NONCE; subsequent scripts injected by the helper carry a nonce the attacker chose. CSP sees a valid nonce and allows the load; the script source is attacker-controlled.
Redirect target override on OAuth flow
The OAuth callback handler reads window.redirectTo as a fallback when the URL fragment is missing. An attacker who controls a markup sink on the same origin plants an anchor with id="redirectTo" before the OAuth redirect lands. The handler reads .href off the anchor and redirects the authenticated session to the attacker domain.
Fetch URL clobbering for SSRF-adjacent leak
A telemetry script reads window.METRICS_ENDPOINT and posts session-scoped data on every page navigation. A clobbered iframe whose name matches METRICS_ENDPOINT redirects the post to an attacker URL. The attacker collects the telemetry payload, which contains user identifiers and feature-flag values.
Reporting and triage in the engagement
DOM clobbering findings are easy to undersell because the proof-of-concept markup looks like a normal anchor or form. The credible report explains the full chain: the sanitiser configuration that allowed the attribute, the global lookup that read it, the downstream sink that acted on it, and the impact on the user or the session. SecPortal's findings management stores the original payload, the rendered HTML, the JavaScript path that read the clobbered value, and the network or DOM action that resulted, so the chain stays attached to the evidence rather than being reconstructed during retest.
Severity calibration matters here. Clobbering of a non-security global is informational; clobbering that lands inside a script src, a fetch URL, an OAuth redirect target, or a sanitiser configuration is high or critical depending on what the chain reaches. The severity calibration research covers how to score chained findings consistently against CVSS and SSVC without double-counting the downstream impact. For the broader workflow that pairs scanner output with manual verification on chain findings, the scanner false positive guide covers the triage discipline.
Retest planning has to cover the whole chain. A sanitiser fix without the JavaScript-side fallback fix leaves the chain alive the next time the sanitiser config drifts; a JavaScript fix without the sanitiser fix leaves the primitive available for a future global the attacker has not targeted yet. The remediation tracking workflow keeps both ends of the chain in scope for verification rather than closing on a one-line fix that addresses the symptom.
Compliance impact
Related vulnerabilities
Catch DOM clobbering chains before they reach the client report
SecPortal probes user-rendered HTML for surviving id and name attributes, flags JavaScript reads against window and document, and keeps the full chain attached to the finding through retest. Start free.
No credit card required. Free plan available forever.