DOM Clobbering
Also Known As
TL;DR
Explanation
DOM clobbering exploits a long-standing browser quirk: HTML elements with `id` or `name` attributes are exposed as named properties on the `document` object and on the global `window`. An attacker who can inject HTML — but not script — can use this to overwrite JavaScript variables and tamper with control flow. Classic primitives: `<a id=foo href=javascript:alert(1)>` clobbers `window.foo` to a link object whose `href` triggers JS when accessed; `<form id=config><input name=apiKey value=evil></form>` makes `window.config.apiKey` return `'evil'` if surrounding JS reads `window.config?.apiKey`. Modern attacks chain DOM clobbering with sanitiser bypasses (DOMPurify pre-3.x had several), prototype pollution, and gadget chains in popular libraries. The defence is layered: never trust that a global variable cannot be redefined; access configuration via closures or modules instead of `window.x`; use Trusted Types where supported; configure your HTML sanitiser to strip `id` and `name` attributes (or restrict them to a safelist). DOM clobbering is most dangerous in applications that allow user-supplied HTML (rich-text editors, comment systems) and read configuration from the DOM.
How It's Exploited
Watch Out
Common Misconception
Why It Matters
Common Mistakes
- Allowing user-controlled `id` or `name` attributes through HTML sanitisers — these are the attack primitives.
- Reading configuration from `document.x` or `window.x` in production code — any element with that id can clobber it.
- Trusting `typeof x === 'undefined'` checks as a way to detect tampering — clobbered globals report as objects, not undefined.
- Forgetting that form children become properties of the form: `<form id=app><input name=user value=admin></form>` makes `app.user` return the input.
- Assuming the latest sanitiser version is safe — DOMPurify, sanitize-html, and others have all had clobbering bypasses; pin and update versions deliberately.
Avoid When
- The application accepts no user-supplied HTML and uses Trusted Types — DOM clobbering surface area is essentially zero.
When To Use
- Auditing applications that allow user-supplied HTML through a sanitiser.
- Reviewing CSP-protected pages that still suffer XSS-class incidents — clobbering is a likely path.
- Sanitiser configuration reviews — checking which attributes are permitted.
Code Examples
<!-- ❌ Reading global config from a DOM element by id — clobberable -->
<div id="config" data-endpoint="/api/v1" data-token="<?= $csrfToken ?>"></div>
<script>
// Attacker-controlled HTML elsewhere on page can clobber window.config
const cfg = window.config || document.getElementById('config');
fetch(cfg.dataset.endpoint + '/orders');
// If user content injects <a id=config href=//evil>, fetch goes to evil.
</script>
<!-- ✅ Read config exactly once into a closure — no global DOM lookup at use-time -->
<script type="application/json" id="app-config">
<?= json_encode(['endpoint' => '/api/v1', 'token' => $csrfToken],
JSON_HEX_TAG | JSON_HEX_AMP) ?>
</script>
<script>
(() => {
const node = document.getElementById('app-config');
const config = Object.freeze(JSON.parse(node.textContent));
// From here on, only `config` (closure variable) is used — not window.config.
function loadOrders() {
return fetch(config.endpoint + '/orders');
}
// Optional hardening: also strip id/name attrs in the user-content sanitiser.
loadOrders();
})();
</script>