GODSON'S BLOG
← Back to Writeups

An Art of DOM Clobbering 🎭

A deep dive into DOM Clobbering from first principles — how id/name attributes create references on window and document, why window properties can't be overridden but document properties can, and how to chain clobbering across multiple levels using nested iframes.

Prerequisites: HTML, a good understanding of JavaScript, and JavaScript prototypes.

What Is DOM Clobbering?

DOM clobbering is a technique where you inject HTML into a page to manipulate the DOM and ultimately change the behavior of JavaScript on that page. It’s particularly useful when XSS isn’t possible, but you can still control some HTML where the id or name attributes are allowed by the HTML filter.

The most common form uses an anchor element to overwrite a global variable that the application later uses unsafely — for example, to generate a dynamic script URL, a pattern described in PortSwigger’s guide to DOM clobbering.

Basics of DOM Clobbering

Before getting into DOM clobbering, we need to cover the id and name attributes.

id attribute: specifies an element’s unique identifier. The value must be unique within the element’s home subtree and must contain at least one character, with no space characters.

Example id attribute

name attribute: specifies a name for an HTML element, which can be used to reference it from JavaScript. For a <form>, it’s used as a reference when data is submitted; for an <iframe>, it can be used to target a form submission.

Example name attribute

Introduction to JavaScript Objects

An object type is simply a collection of properties in the form of name/value pairs. (null and undefined are primitive types, each holding just one value.)

Example object

What is window in JavaScript?

Every browser supports the window object, representing the browser’s window. All global JavaScript objects, functions, and variables automatically become properties of window — global variables are its properties, global functions are its methods.

What is document in JavaScript?

document represents your web page. Accessing any element on an HTML page always starts with accessing document.

Variable Declaration in JavaScript

Variables can be declared with var, let, or const. Variables declared with var are stored under the window object, keyed by variable name:

var myName creating window.myName

Clobbering window Objects

Now that we understand id/name and JavaScript objects, let’s clobber window.

We already know that an id attribute lets us fetch a tag with document.getElementById("<id>") — but it can also be reached as window.<id>. Given <h1 id="hi">Hey!</h1>, we can call either document.getElementById("hi") or simply window.hi.

Exploitation

Hidden toString calls

Sometimes JavaScript calls toString() under the hood without us asking for it. For example, innerHTML-ing an array into body doesn’t append it as a list — the array gets converted to a string first:

Array converted to a string via innerHTML

Template-string interpolation triggers the same implicit toString():

toString() called implicitly in a template string

Array.toString itself calls Array.join under the hood — there are plenty of examples like this. Why does this hidden toString() call matter for us? If you already have reflected HTML injection, it doesn’t. But for DOM-based HTML injection, this hidden call is exactly what lets us exploit it.

Calling toString() on an <h1> returns [object HTMLHeadingElement] — not its inner text:

toString() on an h1 returning [object HTMLHeadingElement]

So we can’t control the result directly — but some fuzzing across tags reveals interesting exceptions:

var allTags = ["a","img", /* ... */]
for (tag of allTags){
  temp = `<${tag} id="test">`;
  document.body.innerHTML = temp;
  try{
    if(!document.getElementById("test").toString().includes("[object")){
      console.log(tag)
    }
  } catch{}
}

Running this shows a and area don’t return [object ...] from toString():

a and area behaving differently

Supplying an href on those tags makes toString() return the href value itself:

href value returned by toString() on an a tag

So we can control the result of toString() for a/area tags (though not fully freely). We also know every id creates a reference on window — what about name? Fuzzing again:

var allTags = ["a","img", /* ... */]
for (tag of allTags){
  temp = `<${tag} name="test">`;
  document.body.innerHTML = temp;
  try{
    if(window.test){
      console.log(tag)
    }
  } catch {}
}

embed, form, iframe, image, img, and object all create a window reference via name:

Tags that clobber window via name

Clobbering document Objects

document doesn’t store arbitrary variables the way window does — it represents the page itself. Can we still clobber it? Fuzzing with id:

var allTags = ["a","img", /* ... */];
for (tag of allTags){
  temp = `<${tag} id="test">`;
  document.body.innerHTML = temp;
  try{
    if(document.test){
      console.log(tag)
    }
  } catch{}
}

object clobbers document via id:

object tag clobbering document via id Confirming the clobbered document property

Fuzzing again with name:

var allTags = ["a","img", /* ... */]
for (tag of allTags){
  temp = `<${tag} name="test">`;
  document.body.innerHTML = temp;
  try{
    if(document.test){
      console.log(tag)
    }
  } catch{}
}

embed, form, iframe, image, img, and object clobber document via name too:

Tags that clobber document via name

For window clobbering, a/area with href let us control the toString() result. Does the same trick work on document?

var mytags = ["embed", "form", "iframe", "image", "image", "object"];
for (tag of mytags){
  temp = `<${tag} name="test">`;
  document.body.innerHTML = temp;
  console.log(document.getElementsByName("test").toString())
}

No — every tag in mytags just returns its element type, unlike a/area, so we can’t control toString() here:

toString() still returning element types for these tags

Overriding Existing Properties Using DOM Clobbering

Can we override pre-existing properties of window or document? Answer: yes for document, no for window — and understanding why requires the JavaScript prototype chain.

The Prototype Chain

The prototype chain is a big topic on its own, best covered by MDN’s guide to inheritance and the prototype chain (or, if you know Tamil, my OWASP talk on prototype pollution covers it in depth).

Take this example:

An object with a name key but no own toString

myObj has a name key, but no toString key — so where does .toString() come from? Its prototype. hasOwnProperty tells us the difference:

hasOwnProperty("name") true, hasOwnProperty("toString") false

name is an own key on myObj; toString comes from Object.prototype. What if we set toString as an own key on myObj?

Setting toString directly on myObj

Did that overwrite Object.prototype.toString? No:

Object.prototype.toString still intact

We can still reach the original toString — printing [object Object].

Edit: IvarsVids pointed out that myObj.__proto__.toString isn’t quite the right way to call this — Object.prototype.toString.call(myObj) is more correct. Thanks, IvarsVids.

Why does that distinction matter? Object, Number, and Array are built-in constructor functions, and all three have a toString on their own prototype:

Object, Number, Array all having toString on their prototypes toString present in each prototype

Even hasOwnProperty itself comes from Object.prototype — it’s searching within Object.prototype for a toString key. A nested example makes the chain clearer:

Nested object also inheriting from Object.prototype

Same idea with arrays — includes comes from Array.prototype:

Array.prototype.includes

myarr.__proto__.includes is the same function as myarr.includes, but calling myarr.__proto__.includes(2) searches inside myarr.__proto__, not myarr. Adding a value directly to Array.prototype shows this clearly:

Adding to Array.prototype directly

toString behaves the same way: String.prototype.toString returns a string, Array.prototype.toString stringifies the array, Object.prototype.toString returns [object <TYPE>]:

Different toString behavior per prototype

So myObj.__proto__.toString() is really Object.prototype.toString.call(myObj.__proto__), not myObj itself.

Why Can’t We Override window Properties?

To see why, we need to know where elements and variables actually live on window.

Where window properties live

If an <h1 id="whoami"> isn’t stored directly on window, why does window.whoami return it? Because of the prototype chain — JavaScript walks it looking for a whoami key:

The prototype chain resolving window.whoami

Tags with an id are stored on window’s prototype, not as window’s own properties — unlike var-declared globals, which are window’s own properties. Since a real declared variable already exists as an own property, DOM clobbering (which only reaches the prototype) can’t shadow it — though we can still read the clobbered value through the chain if the real variable was never declared:

Own properties vs prototype-chain properties on window

When JavaScript can’t find a key on the first __proto__ link, it keeps walking the chain until it hits null. This is exactly why we can’t override existing window properties via clobbering.

Why Can We Override document Properties?

Any of the tags found above work here — let’s use img with name. Object.getOwnPropertyNames(<Object>) lists an object’s own properties:

Object.getOwnPropertyNames(document)

Surprisingly, only location and cookie are document’s own properties — everything else, like document.domain or document.getElementById, comes from its prototype. That means we can add domain as an own key on document: calling document.domain would then return our clobbered value instead of the real one.

Injecting img tags with name shows exactly where those values land:

Clobbered values stored directly as own properties on document

Our keys are stored directly on document as own properties — so per the prototype chain, our tag overrides the original prototype-derived property. Except for document.location, we can clobber almost anything on document. This is great for bypassing checks like if (document.domain != "..."), or breaking functionality by overwriting commonly-used properties:

Example of overriding a document property to break a check

Clobbering More Than One Level

So far this has all been single-property clobbering. But we can clobber multiple levels, like a nested object.

Using the same id on two different tags creates an HTMLCollection:

Two elements sharing an id creating an HTMLCollection

We can combine id and name to build more complex collections — e.g. form tags containing input/output tags, since forms are naturally nested in the DOM tree:

Nested form-based clobbering

(Calling out attributes we injected ourselves this way was exploitable in old IE, but not anymore.)

Terjanq later found a trick to clobber 4 levels deep using nested <iframe>s with srcdoc:

4-level clobbering via nested iframes

An iframe name="a" creates window.a. Its srcdoc injects HTML containing a nested iframe with name="b", reachable as window.a.b. Inside that nested frame, another srcdoc uses two elements sharing id="c" to form an HTMLCollection, reachable as window.a.b.c. Adding a name attribute inside that gets us a fourth level: window.a.b.c.d.

Conclusion

If you want to practice this, check out my challenge at https://ctf.0xgodson.com/chal1/, and the PortSwigger labs too.

Fix

  • Avoid bad code patterns — using global variables together with the logical OR operator (||) is a common source of clobbering gadgets.
  • Use a well-tested library like DOMPurify, which guards against DOM-clobbering vulnerabilities.

Resources

On This Page