DOM Clobbering

Definition

DOM Clobbering is a vulnerability that originates from a naming collision between JavaScript variables and named HTML markups, where browsers replace pre-existing content of an undefined variable with an HTML element when the variable name and the element’s name (or id) attribute match.

Attributes

Attribute id

Example with a random tag with a id attribute:

<h1 id="hd">Super title !</h1>

<script>
console.log(hd);            // <h1 id="hd">Super title !</h1>
console.log(window.hd);     // <h1 id="hd">Super title !</h1>
console.log(document.hd);   // undefined
console.log(hd.toString()); // [object HTMLHeadingElement]
console.log(hd.innerText);  // Super title !
</script>

Attribute name

Example with a form tag with a name attribute:

<form name="fm" method="GET" action="/login"></form>

<script>
console.log(fm);          // <form name="fm"></form>
console.log(window.fm);   // <form name="fm"></form>
console.log(document.fm); // <form name="fm"></form>
console.log(fm.method);   // get
console.log(fm.action);   // http://localhost/login
</script>

List of tags which supports the name attribute:

  • embed, form, iframe, image, img, object

Caution

document.fm (name=fm) is defined but NOT document.hd (id=hd).

Element name fuzzing - Proof of Concept
<body><script>
const tags = ["a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "bdi", "bdo", "bgsound", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame", "frameset", "h1", "head", "header", "hgroup", "hr", "html", "i", "iframe", "image", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "link", "main", "map", "mark", "marquee", "menu", "menuitem", "meta", "meter", "nav", "nobr", "noembed", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "plaintext", "portal", "pre", "progress", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp", "script", "section", "select", "slot", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr", "xmp"];

let valids = [];
tags.forEach(tag => {
	document.body.innerHTML = `<${tag} name="xanhacks"></${tag}>`;
	try {
		if (xanhacks !== undefined)
			valids.push(tag);
	} catch (e) {}
});
console.log(valids); // ['embed', 'form', 'iframe', 'image', 'img', 'object']
</script></body>

Other attributes

Unfortunately, you can only use id or name.

Fuzzing other attributes - Proof of Concept
<body><script>
const tags = ["a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "bdi", "bdo", "bgsound", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame", "frameset", "h1", "head", "header", "hgroup", "hr", "html", "i", "iframe", "image", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "link", "main", "map", "mark", "marquee", "menu", "menuitem", "meta", "meter", "nav", "nobr", "noembed", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "plaintext", "portal", "pre", "progress", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp", "script", "section", "select", "slot", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr", "xmp"];
const attributes = ["accept", "accept-charset", "accesskey", "action", "align", "allow", "alt", "async", "autocapitalize", "autocomplete", "autofocus", "autoplay", "background", "bgcolor", "border", "buffered", "capture", "challenge", "charset", "checked", "cite", "class", "code", "codebase", "color", "cols", "colspan", "content", "contenteditable", "contextmenu", "controls", "coords", "crossorigin", "csp", "data", "data-*", "datetime", "decoding", "default", "defer", "dir", "dirname", "disabled", "download", "draggable", "enctype", "enterkeyhint", "for", "form", "formaction", "formenctype", "formmethod", "formnovalidate", "formtarget", "headers", "height", "hidden", "high", "href", "hreflang", "http-equiv", "id", "integrity", "intrinsicsize", "inputmode", "ismap", "itemprop", "keytype", "kind", "label", "lang", "language", "loading", "list", "loop", "low", "manifest", "max", "maxlength", "minlength", "media", "method", "min", "multiple", "muted", "name", "novalidate", "open", "optimum", "pattern", "ping", "placeholder", "playsinline", "poster", "preload", "readonly", "referrerpolicy", "rel", "required", "reversed", "role", "rows", "rowspan", "sandbox", "scope", "scoped", "selected", "shape", "size", "sizes", "slot", "span", "spellcheck", "src", "srcdoc", "srclang", "srcset", "start", "step", "style", "summary", "tabindex", "target", "title", "translate", "type", "usemap", "value", "width", "wrap"];

let valids = [];
tags.forEach(tag => {
	attributes.forEach(attribute => {
		if (attribute != "id" && attribute != "name") {
			document.body.innerHTML = `<${tag} ${attribute}="xanhacks"></${tag}>`;
			try {
				if (xanhacks !== undefined)
					valids.push([tag, attribute]);
			} catch (e) {}
		}
	});
});
console.log(valids); // [] - Empty array
</script></body>

Values

One level deep

Use an anchor with the href attribute:

<a id="link" href="xyzxyz" lang="fr" class="blue"></a>

<script>
console.log(link);            // <a id="link" href="xyzxyz"></a>
console.log(link.toString()); // http://localhost/xyzxyz
console.log(link.href);       // http://localhost/xyzxyz
console.log(link.value);      // undefined
console.log(link.lang);       // fr
console.log(link.class);      // undefined
</script>

You can retrieve some attributes like href and lang but not class. You can list all the attributes you can retrieve on a specific element by doing fuzzing.

Attribute enumeration - Proof of Concept
<body><script>
const tags = ["a", "abbr", "acronym", "address", "applet", "area", "article", "aside", "audio", "b", "base", "bdi", "bdo", "bgsound", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "data", "datalist", "dd", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "em", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame", "frameset", "h1", "head", "header", "hgroup", "hr", "html", "i", "iframe", "image", "img", "input", "ins", "kbd", "keygen", "label", "legend", "li", "link", "main", "map", "mark", "marquee", "menu", "menuitem", "meta", "meter", "nav", "nobr", "noembed", "noframes", "noscript", "object", "ol", "optgroup", "option", "output", "p", "param", "picture", "plaintext", "portal", "pre", "progress", "q", "rb", "rp", "rt", "rtc", "ruby", "s", "samp", "script", "section", "select", "slot", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt", "u", "ul", "var", "video", "wbr", "xmp"];
const attributes = ["accept", "accept-charset", "accesskey", "action", "align", "allow", "alt", "async", "autocapitalize", "autocomplete", "autofocus", "autoplay", "background", "bgcolor", "border", "buffered", "capture", "challenge", "charset", "checked", "cite", "class", "code", "codebase", "color", "cols", "colspan", "content", "contenteditable", "contextmenu", "controls", "coords", "crossorigin", "csp", "data", "data-*", "datetime", "decoding", "default", "defer", "dir", "dirname", "disabled", "download", "draggable", "enctype", "enterkeyhint", "for", "form", "formaction", "formenctype", "formmethod", "formnovalidate", "formtarget", "headers", "height", "hidden", "high", "href", "hreflang", "http-equiv", "id", "integrity", "intrinsicsize", "inputmode", "ismap", "itemprop", "keytype", "kind", "label", "lang", "language", "loading", "list", "loop", "low", "manifest", "max", "maxlength", "minlength", "media", "method", "min", "multiple", "muted", "name", "novalidate", "open", "optimum", "pattern", "ping", "placeholder", "playsinline", "poster", "preload", "readonly", "referrerpolicy", "rel", "required", "reversed", "role", "rows", "rowspan", "sandbox", "scope", "scoped", "selected", "shape", "size", "sizes", "slot", "span", "spellcheck", "src", "srcdoc", "srclang", "srcset", "start", "step", "style", "summary", "tabindex", "target", "title", "translate", "type", "usemap", "value", "width", "wrap"];

let valids = [];
tags.forEach(tag => {
	attributes.forEach(attribute => {
		if (attribute != "id" && attribute != "name") {
			document.body.innerHTML = `<${tag} id="xyz" ${attribute}="xanhacks"></${tag}>`;
			try {
				if (eval(`xyz.${attribute} == "xanhacks"`))
					valids.push([tag, attribute]);
			} catch (e) {}
		}
	});
});
console.log(valids); // [...] - a lot
</script></body>

Two levels deep

You can clobber two depths variable using HTMLCollection:

<a id="link"></a>
<a id="link" name="add" href="xyzxyz"></a>

<script>
console.log(link);                // HTMLCollection(2) [a#link, a#link, link: a#link, add: a#link]
console.log(link.add);            // <a id="link" name="add" href="xyzxyz"></a>
console.log(link.add.toString()); // http://localhost/xyzxyz
console.log(link.add == link[1]); // true
</script>

Caution

HTMLCollection only works on Chromium based browser (not Firefox).

You can also create an array of values:

<a id="link"></a>
<a id="link" name="add" href="xyzxyz"></a>
<a id="link" name="add" href="abcabc"></a>

<script>
console.log(link[1].toString()); // http://localhost/xyzxyz
console.log(link[2].toString()); // http://localhost/abcabc
</script>

Three levels deep

<form id="login" name="user">
	<input id="name" value="xanhacks">
</form>
<form id="login">

<script>
console.log(login.user.name);            // <input id="name" value="xanhacks">
console.log(login.user.name.toString()); // [object HTMLInputElement]
console.log(login.user.name.value);      // xanhacks
</script>

Infinite levels deep

Caution

iframe allows you to clobber as many levels as you want. However, iframes are often blocked by HTML filters.

This simple example does not work because the iframe takes some time to render:

<iframe name="page"
	srcdoc="<a id='link' href='xanhacks'></a>">
</iframe>

<script>
console.log(page.link); // undefined
</script>

To make things works, you can add some delay by adding a CSS import:

<iframe name="page"
	srcdoc="<a id='link' href='xanhacks'></a>">
</iframe>
<style>@import "https://xanhacks.xyz"</style> <!-- add delay -->

<script>
console.log(page);                 // Window {window: Window, self: Window, ... }
console.log(page.link);            // <a id="link" href="xanhacks"></a>
console.log(page.link.toString()); // http://localhost/xanhacks
</script>

References