The browser's postMessage API deliberately lets windows from different origins talk to each other, so every message listener is a cross-origin entry point that must decide for itself who it trusts. Chat widgets, payment frames, embedded editors, analytics tags and single sign-on popups all use it, and the defects are remarkably consistent: listeners that never check event.origin, origin checks written with substring matching, senders that broadcast tokens with a "*" target, and message data written straight into the DOM. This guide explains how cross-window messaging works, how to validate origin, source and payload properly, how to design widget and SSO popup protocols, how sandboxing, MessageChannel and Cross-Origin-Opener-Policy change the picture, and how to find weak listeners. It is for front-end developers, identity engineers building popup sign-in flows, and reviewers testing third-party embeds, and it goes deeper on one topic from the Client-Side Security Guide.
Key takeaways
- No listener is the safest listener. MDN's guidance is blunt: if you do not expect messages from other sites, do not add
messageevent listeners at all. - Check origin by exact match. Compare
event.originagainst a fixed allowlist of full origins (scheme, host and port);indexOf,includes,endsWithand loose regular expressions all let attacker domains through. - A trusted origin does not mean trusted data. Validate the message schema too, because an XSS flaw on the trusted sender lets an attacker send any message.
- Never send secrets to "*". A sender must name the exact
targetOrigin, because the target window can be navigated to an attacker page without the sender knowing. - Bind messages to a specific window and session. Check
event.source, use a per-flow nonce, and move to a privateMessageChannelafter the handshake.
How cross-window messaging works
A script with a reference to another window (an iframe's contentWindow, window.parent, window.opener or the return value of window.open()) calls otherWindow.postMessage(message, targetOrigin, transfer) or the options form postMessage(message, { targetOrigin, transfer }). The browser serialises the message with the structured clone algorithm and delivers a MessageEvent to the target only if the target document's origin matches targetOrigin exactly, including scheme, host and port. The receiver sees three security-relevant properties.
| Property or argument | Meaning | Security note |
|---|---|---|
targetOrigin (sender) | The origin the recipient must have; defaults to "/" (the sender's own origin); "*" means any | The only control the sender has over who reads the message |
event.origin (receiver) | Origin of the sending window when postMessage was called | Set by the browser, cannot be forged by page script; MDN notes it is not guaranteed to be the window's current or future origin |
event.source (receiver) | Reference to the sending window | Lets the receiver confirm the message came from the specific frame or popup it opened, and reply to it |
event.data (receiver) | The cloned message | Attacker-controlled unless origin and schema checks pass |
event.ports (receiver) | Transferred MessagePort objects | Used to set up a private channel |
Any window able to obtain a reference can post to any other. MDN points out that any window, including a malicious one, can send a message to any frame in the iframe hierarchy beneath the current document, and an attacker page can open your page in a popup or frame it (where framing is allowed) to get that reference.
Related messaging channels
| API | Scope | Main risk |
|---|---|---|
window.postMessage | Cross-origin, between window references | Missing origin checks, wildcard targets |
MessageChannel / MessagePort | Point-to-point between two contexts after a port is transferred | The initial port handoff still needs a correct targetOrigin |
BroadcastChannel | Same-origin contexts (tabs, frames, workers) | Any same-origin page, including one with an XSS flaw, can read and send |
Worker postMessage | Between a page and its worker | Treat worker results as untrusted if the worker processes external data |
| Browser extension messaging | Between page and extension content scripts | Pages can send messages that extensions trust too readily |
Validating the sender: origin and source
MDN states that failure to check the origin and possibly source properties enables cross-site scripting. ASVS 5.0 requirement V3.5.5 requires that messages received by the postMessage interface are discarded if the origin is not trusted or the syntax is invalid.
| Check | Verdict | Why |
|---|---|---|
| No check | Vulnerable | Any site can drive the listener |
event.origin.indexOf("example.com") !== -1 | Vulnerable | Matches https://example.com.attacker.net; OWASP's HTML5 cheat sheet gives this exact pattern as unsafe |
event.origin.endsWith("example.com") | Vulnerable | Matches https://evilexample.com |
/https:\/\/.*\.example\.com/ | Vulnerable | Unanchored, and the unescaped dot and wildcard match far more than intended |
event.origin.startsWith("https://example.com") | Vulnerable | Matches https://example.com.attacker.net |
ALLOWED.has(event.origin) with a Set of full origins | Correct | Exact string comparison of scheme, host and port |
Exact origin plus event.source === expectedWindow | Best | Also rejects other frames or tabs from the same trusted origin |
- Write the allowlist as full origins:
https://pay.example.com, not a host name. Include the port only where it is non-default, and never includehttp:variants in production. - Handle the opaque origin: sandboxed iframes without
allow-same-originanddata:documents have an opaque origin, which arrives as the string"null". Never put"null"on an allowlist, because any attacker can create a sandboxed frame that sends it. - Check source when you know the window: a parent that created an iframe should compare
event.sourcewithiframe.contentWindow; an opener should compare with the popup reference it holds. - Check before any work: return immediately on failure, before parsing, logging large payloads or changing state.
- Subdomain trust is a decision: accepting every subdomain means any subdomain takeover or user-content host can talk to the listener. List them explicitly.
Validating the message: schema and safe handling
Origin checks establish who sent a message, not whether it is safe. MDN warns that a security hole in a trusted sender could otherwise open a cross-site scripting hole in your site, and OWASP adds that an XSS flaw on the sending page lets an attacker send messages in any format.
- Define a protocol: each message carries a
typefrom a fixed set, a protocol version and only the fields that type allows. Reject unknown types, extra properties and wrong data types. - Treat data as data: never pass message content to
eval(),new Function,setTimeoutwith a string,innerHTMLordocument.write; write text withtextContent. ASVS 5.0 V3.2.2 and V1.3.2 cover safe rendering and dynamic code. See the Trusted Types and DOM XSS Prevention Guide. - Validate URLs in messages: a message that says "navigate to" or "load this script" is an open redirect or script injection unless the URL is parsed with
new URL()and checked against allowed schemes and hosts. See the Open Redirect Guide. - Do not dispatch on message content: patterns such as
window[event.data.fn](event.data.args)let the sender call any global function. Map types to handlers with a fixed table. - Beware of prototype keys: merging
event.datainto configuration objects can pollute prototypes. See the Prototype Pollution Guide. - Bound size and rate: ignore oversized messages and throttle bursts so a hostile frame cannot freeze the page.
Sending safely: targetOrigin and what to send
- Always set an exact targetOrigin: MDN warns that a malicious site can change the location of the window without your knowledge and intercept data sent with
"*". For replies, useevent.source.postMessage(reply, event.origin)only afterevent.originpassed the allowlist. - Know the exceptions: MDN notes that sending to a
data:orfile:context currently requires"*". That is a reason to avoid such targets for anything sensitive, not a reason to use"*"elsewhere. - Send the minimum: an embedded frame rarely needs a full access token. Send a short-lived, audience-restricted value or an authorisation code that the receiving server exchanges.
- Do not broadcast to all frames: loops that post the same message to every
window.frames[i]leak to any third-party frame on the page. - Re-check before sending later: if a reply is sent after asynchronous work, the target may have navigated; the exact
targetOriginis what makes that safe.
Widget, payment and SSO popup patterns
| Scenario | Typical flaw | Secure design |
|---|---|---|
| Embedded widget (chat, support, analytics) talking to its host page | Widget accepts commands from any parent; host page accepts any child message | Widget reads its allowed host origins from server configuration per customer; both sides check origin and source; commands limited to a small typed set |
| Payment or card-entry iframe | Parent sends configuration with "*"; frame posts results to "*" | Frame posts only to the merchant origin registered for the session; card data never leaves the frame; the server confirms payment status rather than trusting the message |
| SSO or OAuth popup returning a result to the opener | Popup posts tokens to window.opener with "*"; opener accepts results from any origin | Popup posts to the registered client origin only; result carries the state or nonce the opener generated; the opener verifies origin, source === popup and the nonce; prefer returning an authorisation code that the backend exchanges |
| Silent token renewal in a hidden iframe | Listener on every page accepts tokens | Listener installed only for the renewal, removed after one valid message or a timeout |
| Micro-frontends on different subdomains | Wildcard subdomain trust | Explicit origin list per frontend and a versioned message schema |
For identity flows, the nonce matters as much as the origin: without it, an attacker page that opens the popup itself can receive or inject a result. See the OAuth for Browser-Based Applications and the BFF Pattern Guide and the Identity Provider and SSO Security Guide.
Isolation tools: sandbox, MessageChannel and COOP
- Sandboxed iframes: the
sandboxattribute treats framed content as a unique origin unlessallow-same-originis set, and blocks scripts unlessallow-scriptsis set. MDN strongly discourages combiningallow-scriptsandallow-same-originfor same-origin content, because the frame can then remove its own sandbox. Serve untrusted content from a separate origin. - MessageChannel after a handshake: the parent creates a
MessageChannel, transfersport2to the verified frame with an exacttargetOrigin, and from then on both sides use their ports. Other windows cannot post into that channel, so the globalmessagelistener can be removed after the handshake. - Cross-Origin-Opener-Policy:
same-originplaces the document in its own browsing context group and severswindow.openerreferences to cross-origin popups and openers, which stops attacker pages scripting or messaging your window through those references.same-origin-allow-popupskeeps references to popups you open, which suits a relying party that opens an identity provider popup. ASVS 5.0 V3.4.8 asks for COOP on document responses. See the Permissions Policy, COOP, COEP and CORP Guide. - Framing controls: CSP
frame-ancestorslimits who can embed your page and so who can get acontentWindowreference to it. See the Clickjacking Guide.
Finding weak listeners and measuring progress
- Inventory: search first-party code for
addEventListener("message",onmessage,postMessage(and"*"targets; in the browser, developer tools list the event listeners registered onwindow, including those added by third-party scripts. - Classify: for each listener record the expected senders, the message types handled and what each type can change (DOM, navigation, storage, tokens).
- Review: confirm exact origin checks,
sourcechecks where possible, schema validation, and no dangerous sinks. Static analysis rules for missing origin checks help at scale. See the Secure Code Review Guide. - Test: in an authorised test, host a page on an unlisted origin and on look-alike origins, frame or open the target, and confirm every message is ignored; repeat from a sandboxed frame to confirm
"null"is rejected. - Fix and roll out: ship a shared listener helper that enforces the allowlist and schema, migrate listeners to it, and add a lint rule that bans raw listeners and
"*"targets. Roll back per feature flag if a legitimate partner origin was missed.
Metrics:
- Message listeners without an exact origin allowlist (target zero).
postMessagecalls using"*"with non-public data (target zero).- Share of listeners using the shared validation helper.
- Third-party scripts that register
messagelisteners, reviewed at onboarding. See the Third-Party JavaScript Management Guide. - Rejected-message counts from client telemetry, which show probing or misconfigured partners.
Common failure patterns
- The missing check: a listener added "temporarily" for a partner integration trusts every message and writes
event.data.htmlinto the page. - Substring origin checks:
origin.includes("example.com")acceptsexample.com.attacker.net, and the review passes because a check exists. - Tokens posted to "*": an SSO popup or embedded login frame sends an access token to whatever window opened it, so an attacker page that opens the popup receives the token.
- Trusting "null": a listener allows
"null"to support a sandboxed preview frame, which any attacker can also create. - Navigation by message: a widget accepts
{type: "redirect", url}and assigns it tolocation.href, enablingjavascript:URLs and phishing redirects. - Listener that never goes away: a one-time handshake listener stays registered for the page's lifetime and accepts replayed results.
Framework mapping
| Control | OWASP ASVS 5.0 | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 |
|---|---|---|---|
| Discard messages from untrusted origins or with invalid syntax | V3.5.5 | SI-10, AC-4 | PR.PS-06 |
| Safe rendering of message data | V3.2.2, V1.3.2 | SI-10 | PR.PS-06 |
| Opener isolation with COOP | V3.4.8 | SC-7 | PR.IR-01 |
| Minimal, audience-bound tokens in messages | V14.2.1 | SC-8, IA-5 | PR.DS-02 |
For threat modelling, ATT&CK T1059.007 JavaScript describes the script execution an injectable listener enables, and T1539 Steal Web Session Cookie the session theft that follows. ISO/IEC 27001:2022 Annex A control A.8.28 Secure coding covers the coding rules.
Where identity fits
Cross-window messages often carry identity material: ID tokens and access tokens from SSO popups, session handoff codes between a portal and an embedded app, and API keys that a host page passes to a third-party widget so it can call the vendor's backend. Each of those is a credential crossing an origin boundary. Prefer designs where the browser only ever carries a one-time code bound to the client and the nonce, exchanged server side for a token; where a widget needs a vendor API key, issue a publishable key scoped to that customer origin rather than a secret key, and rotate it if it was ever posted to "*". See the API Key Management Guide.
Practitioner checklist
- Every
messagelistener is inventoried with its expected senders and message types, and unneeded listeners are removed. - Origins are checked by exact match against a
Setof full origins; no substring, suffix or unanchored regex checks remain. "null"is never an allowed origin.event.sourceis compared with the expected frame or popup wherever the receiver holds a reference.- Every message is validated against a typed schema and handled through a fixed type-to-handler table.
- Message data never reaches
eval,innerHTML, navigation or script URLs without validation. - Senders always use an exact
targetOrigin; no sensitive data is posted to"*". - SSO and payment popups bind results to a per-flow nonce and prefer codes over tokens.
- Long-lived conversations move to a
MessageChannelafter a verified handshake. - COOP,
frame-ancestorsand sandboxed iframes on a separate origin limit who can obtain window references.
Standards and references
- MDN: Window postMessage() method
- OWASP: HTML5 Security Cheat Sheet
- OWASP: Application Security Verification Standard 5.0
- MDN: Using channel messaging
- MDN: Cross-Origin-Opener-Policy header
- MDN: The Inline Frame element (iframe)
Related NHI Mgmt Group resources: Client-Side Security Guide · Permissions Policy, COOP, COEP and CORP Guide · Trusted Types and DOM XSS Prevention Guide · TypeScript and JavaScript Secure Coding Guide