postMessage XSS: Finding Origin Bugs in Browser Messaging
window.postMessage lets frames talk. Developers paste listeners that trust event.data and forget event.origin. You send a message from an attacker page into a vulnerable opener/iframe and suddenly DOM XSS or token exfil is in play — often past CSP that only thought about script tags in HTML.
Spotting listeners
Search JS for addEventListener("message", onmessage, and wrappers in analytics/chat widgets. Browser DevTools → Event Listeners on window also works. Note whether code checks event.origin against an allowlist, uses *, or verifies a custom token inside the payload only (spoofable).
Embedded third parties (support chat, payment iframes, SSO popups) are frequent offenders.
Attack sketch
- Find a page that installs a loose listener and does something dangerous with
data—innerHTML,eval,location,document.write, or passes URLs into sinks. - Host an HTML file that opens/embeds the victim origin (within scope rules) and calls
target.postMessage(payload, "*")or the victim origin. - Prove script execution or sensitive read in your session.
If the listener accepts * as targetOrigin when sending, that is a separate footgun for the app’s outbound messages (token leakage to embeds).
Payloads that teach the sink
Start with a unique string marker. Escalate to HTML only after you see reflection into HTML sinks. For JSON APIs inside messages, try prototype-ish keys only when relevant — most wins are straight HTML/JS injection via naive parse-and-assign.
Impact talk
DOM XSS on the main app origin beats XSS inside a sandboxed iframe with a unique origin. Steal localStorage session material only on accounts you own. Chain with open redirects that land on the vulnerable listener page.
Fixes for the report footer — when you want one
Validate event.origin strictly. Type-check event.data. Prefer event.source postMessage replies to known windows. Avoid innerHTML on message contents. Set explicit targetOrigin when sending.
Not every message listener is XSS. Look for missing origin checks plus a real sink. That pairing is the bug — and it is still hiding in checkout widgets and legacy embeds across 2026 targets.