Clobbering the clobbered — Advanced DOM Clobbering

Based on @SecurityMB XSS Challenge

This is a write-up for an XSS Challenge that popped out on Twitter recently. In this article, I will talk through three different approaches that one could take to solve the challenge, including the shortest among the submitted solutions. The latter resulted in a surprising discovery of how HTML is parsed.

XSS Challenge

It was supposed to be a mini-article but turned out to be an at least medium-size text. Enjoy reading! :)

Task description

The beauty of the XSS Challenges occasionally popping out on Twitter, comes from their simplicity while still preserving high exploitation complexity. Michał’s challenge is no exception to that rule.

We have a website whose main functionality is rendering user-controlled input inside an iframe, then creating a unique URL that could be shared. From the challenge description:

- Please enter some HTML. It gets sanitized and shown in the iframe.
- The task is: execute alert(1) (it must actually execute so you have to bypass CSP as well).
- The solution must work on current version of at least one major browser (Chrome, Firefox, Safari, Edge).
- If you find a solution, please DM me at Twitter:
- DOMPurify has been updated to 2.0.1 so you cannot exploit
the latest mXSS.

The goal is clear:

DOMPurify and full bypass

As mentioned in the description, the user’s input is sanitized using the DOMPurify sanitizer which converts dangerous code into a safe one. It means that there is no easy way to inject any executable code. However, Michał also found a full bypass to that via mXSS in Chrome which was patched in v2.0.1 and also updated in the challenge to the latest version.

DOMPurify bypass

Injection point

We can see how our input is processed in the code snippet included below.

Part of the index.html

The user’s input goes to the ${sanitized} placeholder and may contain most of the safe HTML tags such as <p> or <b> because DOMPurify allows them by default. A full list of allowed tags and attributes can be found in the library source code.

A less relevant part of the challenge, but yet very important, is the Content-Security-Policy rule that restricts including scripts to only ones from and domains (and respectively /how-can-i-escape-this/ and /xss/1/modules/v20190816/ paths) or using scripts with nonce=xyz attribute.

DOM Clobbering — introduction

In the code from the previous section, we can also notice that the main.js script is being included right after the code controlled by the user. Contents of that script are shown below.

Contents of the main.js script

The purpose of the code is simple — it includes two other modules (h1-magic.js and tracker.js). But the crucial part of the code starts on line 12:

If we could control CONFIG.test, window.testPath, window.testPath.protocol, and CONFIG.version values we would take over the control of the whole URL from which the modules are loaded.

And we can, because the code is vulnerable to a technique called DOM Clobbering which allows defining certain variables in the window’s context (the technique was greatly explained in Google CTF 2019 Write-up video which is a write-up for a similar challenge).

For example, an HTML element <p id="CONFIG"></p> creates a reference to itself under a window.CONFIG variable if and only if there were no other variables declared with the same name.

CSP Path bypass

Before I move into the solution, I would like to mention that it is possible to bypass the Content-Security-Policy when including a raw file from We can find a tweet about that on Michał’s Twitter wall.

By following the instructions, I created a paste with alert(1) as a body which when accessed through should work as described in the tweet. This bypasses the CSP rule if applied correctly.

DOM Clobbering — straight-forward solution

Just to recap, the goal is to achieve a DOM XSS by defining certain variables in the window context, which have been introduced in the ‘DOM Clobbering — introduction’ section.

I searched for HTML Elements that contain protocol, host and version attributes and found out that <area> and <a> define the first two and <html> the latter. However, neither the <html> element nor the <a path=xxx protocol=xxx> can be successfully injected into the resulting iframe due to a rigorous code sanitization (via DOMPurify).

First bypass: the protocol and host

A helpful fact is that the browser automatically defines the protocol and host attributes on anchor elements. Hence, to define them we only need to create an anchor tag <a> with the href attribute pointing to the desired domain. Given that, the inserted element may look like <a href="" id="testPath"> which returns https:// as the protocol and as the host.

Nested objects in a form: fail

I didn’t find a way to successfully inject <html> elements into the resulting iframe. Neither did I find other elements having the version attribute. The reason we would like to define this variable is that we need to traverse back from the /xss/1/modules/${CONFIG.version}/${moduleName}.js path to the root / and create a URL that will download the prepared alert(1) script using the technique described in the ‘CSP Path bypass’ section.

However, it is possible to define nested objects via <form> for example. By injecting the code <form id="CONFIG"><input name="test"><input name="version"> we can access the <input> elements through CONFIG.version and CONFIG.test references. While the latter reference will evaluate to true, the former will not return what we need. It would result in crafting the following URL:[object HTMLInputElement]/h1-magic.js which is not the URL required to download the prepared script from.

Second bypass: custom attributes

After looking for elements that return something different from [object ...] when being accessed through a reference, I discovered that <a> elements return href instead. However, attempting to insert <form id="CONFIG"><a name="version"> into DOM results in CONFIG.version returning undefined because of the anchor <a> not being a valid node for a <form> element.

Third bypass: the collection

I was stuck on defining a custom CONFIG.version for a while, but then I noticed that there is another way to perform the DOM Clobbering. We can insert two elements with the same id which creates a reference to a HTMLCollection object instead of a simple HTMLElement. Moreover, defining name attributes altogether allow accessing these elements through HTMLCollection.<name> on chromium-based browsers.

Inserting the code <a id="CONFIG" name="test"><a id="CONFIG" href="" name="version"> into DOM results in CONFIG yielding HTMLCollection as shown on the image.

With that, casting CONFIG.version to string returns, and casting CONFIG.test to boolean returns true.

The Complete Solution

The last missing piece is to traverse back to the/how-can-I-escape-this path and append the bypassing CSP suffix %2f..%2fraw/LiE18yqs?. An obvious attempt was to insert an anchor <a href="https://../../../../how-can-escape-this%2f..%2fraw/LiE18yqs?" id="CONFIG" name="version"> into DOM but doing so made CONFIG.version return https://../how-can-escape-this%2f..%2fraw/LiE18yqs? with only one pair of ../, which is not enough to traverse back and download the script.

To bypass this, I used a cid: protocol (one of the least schemas allowed by DOMPurify) which returns an unmodified URL. By bringing all the pieces together, I crafted the following payload.

Straight-forward solution (Chromium-based only)

After inserting the above code into the destinated iframe on the challenge’s website, the browser requested a script with the URL (which is a folded version of and then popped out an alert which was the goal of the challenge. That means the challenge was successfully solved and I have been added into the challenge Hall of Fame! :)

Hall of fame of the challenge solvers

Clobbering the clobbered

Meanwhile, @sirdarckcat extended the challenge with the golfing idea in the tweet.

I took up the challenge and according to the built-in counter, I managed to achieve the shortest payload using only 212 characters!

To achieve that, I explored interesting areas and discovered strange browsers’ behaviors that led me to a significant decrease in the solution length. Without further ado here is the thing I am talking about including a draft of the final payload, all that in a form of the colorful diagram.

Reduction: One

The very first subtlety I noticed was that version and host attributes don’t need to be defined at all. The protocol attribute can be overridden the same way I defined the version in my original payload.

With that addition, I don’t need to use path traversal because I can just create an anchor <a href=// id=testPath name=protocol> which yields the correct prefix from the beginning and the suffix will be ignored because of the ? character at the end.

Reduction: Two

The second observation is that we can use <form> to save some characters on defining the same id=CONFIG twice but also that the <img> is a valid child of <form> and so can be accessed via reference. The shortest payload I crafted fulfilling these first two reductions is:

Solution 225 (Chromium-based only)

This is a 225-character long solution (based on the built-in counter) that doesn’t use anything we haven’t mentioned so far and which seemed to be the second shortest solution at the time of writing this article, just after my 212 one.

Reduction: Three

I played a bit with the code, searching for improvements and discovered a surprising behavior. Inserting the following combination into the DOM after being sanitized by DOMPurify: <a href=foo><form><a> results in the <a> element getting cloned with the preservation of all attributes! The unfolded version of the code can be seen on the image below.

Unfolded <a href=foo><form><a>

Amazing, right?

At first, I thought I discovered a bug in the DOMPurify library and took some time trying to exploit this strange behavior. Later, I noticed that it is browsers that do the actual magic. After a small investigation, I concluded that this happens because of unfolding opened but unclosed tags mixed with attempts to get the best possible match. Although I didn’t find any information describing this behavior, it remains to be all but speculation.

Bug or not, it enabled a way to craft a 214-character long solution!

Solution 214 (Chromium-based only)

The Final Reduction

I did not stop looking deeper into this unexpected, but yet very interesting, behavior and managed to reduce the solution’s length further. Instead of using <form> to clone an element, I used <p> tags. The combination <a><p><a><p>T clones anchor elements twice and unfolds to:

Unfolded <a><p><a><p>T

By combining all the pieces, I managed to reduce the length of the solution by two characters and crafted the winning 212-character short payload!

Solution 212! (Chromium-based only)

By accessing the following URL you can see the payload in action, but keep in mind that it was only confirmed to be working on Chrome browser.

Security enthusiast that loves playing CTFs and hunting for bugs in the wild. Also likes to do some chess once in a while.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store