JavaScript postMessage Table

PieceWhat it doesField note
window.postMessage(msg, targetOrigin)The sendCross-realm fire-and-forget; post at contentWindow, not the iframe element
message eventThe receivee.origin (sender), e.data (cloned), e.source (reply window) - validate BEFORE trusting
e.sourceThe reply channele.source.postMessage(reply, e.origin) answers the exact sender, no stored refs
structured cloneWhat crossesData copied, not shared - functions/prototypes/DOM do not survive; Maps/Blobs do
targetOriginThe delivery gateBrowser delivers only if the receiver's origin matches the exact string you named
'*' + secretsThe classic leakWildcard delivers tokens/user data to ANY embedder - whatever iframed you today
iframe <-> parentThe main sceneResponsive embeds, SSO popups, widget bridges - validate origin, version payloads
vs BroadcastChannelThe siblingpostMessage = point-to-point across origins; BroadcastChannel = same-origin pub/sub
Reference: the MDN Window.postMessage() method. The API solves one problem - talking across a trust boundary the same-origin policy deliberately enforces - and its security model reflects that: the browser will not deliver your message to the wrong origin (targetOrigin), but it will happily deliver ANYONE's message to you, which is why the receiving side owns authentication.
Bottom line: the browser moves the mail; it does not check the sender's ID. Validate e.origin against an exact allowlist (equality, never substring), version your payload shapes so old embedders cannot misread new messages, reply through e.source at e.origin instead of assuming window.parent, and never send secrets with '*' - the wildcard hands your payload to whatever page embeds you right now, which after one redirect is not necessarily your partner.
Related tools: the BroadcastChannel table (the same-origin sibling - choose by trust boundary), the structured clone table (what actually crosses), the CORS table (the HTTP-side of the same policy), and the Web Workers table (the other window your messages may target).

postMessage is the only sanctioned channel between two realms that may not trust each other: window to iframe, iframe to parent, opener to popup, your page to a third-party widget. The payload travels asynchronously and arrives as a structured clone - which means it works across origins, where every other direct object reference is blocked by the same-origin policy.

Bottom line: the browser enforces exactly one thing for you - targetOrigin gates DELIVERY, meaning the message only arrives if the receiving window's origin matches the string you named. Everything else is your job: authenticate the sender by checking e.origin against an allowlist before trusting e.data, reply through e.source instead of assuming window.parent, and version your payload shapes so an old embedder cannot misread a new message. The browser moves the mail; it does not check the sender's ID.

The honest part: two failure modes dominate production incidents. The wildcard leak - posting tokens or user data with targetOrigin '*' delivers your secrets to whatever page currently embeds you, which after a redirect or an open redirect chain is not necessarily your partner. And the missing protocol - postMessage has no return value, so teams bolt on globals or polling instead of a 10-line request-id convention. Both are architecture decisions made once, on day one.

How to use

  1. Send with an exact origin: iframeEl.contentWindow.postMessage({ type: 'resize', height: 900 }, 'https://embed.partner.com') - the full scheme-plus-host string, never '*' when the payload matters. Note you post to contentWindow (the window), not the iframe element itself.
  2. Receive with an allowlist: window.addEventListener('message', (e) => { if (e.origin !== 'https://embed.partner.com') return; handle(e.data); }) - the check is equality against a known origin, not a substring test (containment checks like .includes('partner.com') are defeated by partner.com.evil.io).
  3. Reply through the event: e.source.postMessage({ type: 'resized' }, e.origin) - event.source is the window that sent you the message and e.origin is where it claims to live; replying there needs no stored window references and cannot reach the wrong frame.
  4. Build request/response on ids: postMessage returns nothing, so put an id in the request and correlate it in the reply - { type: 'get-height', id: 42 } answered by { type: 'height', id: 42, value: 900 } - then resolve a stored Promise per id. Ten lines that replace every global-variable bridge ever written.
  5. Know the main scenes: responsive embeds (iframe reports scrollHeight up, parent sets height), OAuth/SSO popup handoffs (opener receives the token message - which is why the redirect URI and origin checks are security-critical), editor/widget bridges, and worker-to-window relays. Each scene is the same loop: validate origin, version payload, reply to source.

Frequently asked questions

Is postMessage secure?

It is as secure as your e.origin check, and no more. Delivery is gated by targetOrigin (you choose who may RECEIVE), but receiving is not: any page can post a message at your window with whatever shape it likes, so the sender side of trust lives entirely in your allowlist-plus-schema validation. The two classic failures both skip that: trusting e.data because 'it must be our widget', and sending secrets with '*' - which hands the payload to any embedder, including a lookalike that iframed you. Post with exact origins, receive with exact allowlists, and treat e.data as hostile input even when it looks familiar.

postMessage vs BroadcastChannel vs the storage event - which when?

Choose by trust boundary, not features. Different origins (your page and a partner widget, a popup you opened): postMessage - it is the only cross-origin option, point-to-point, needs a window reference (contentWindow, opener, or e.source). Same origin, different tabs (a form preview, multi-tab logout): BroadcastChannel - pub/sub across all tabs of the origin with zero window references and no origin validation to write. The storage event: same-origin tab sync too, but stringly-typed through localStorage with awkward semantics - use it only if you already persist that state. Cross-origin and same-origin are different problems wearing similar APIs.

Can I send DOM nodes, functions, or class instances?

No - the payload is structured-clone copied, not shared: functions, DOM nodes, prototypes, and error-bearing circular class graphs do not survive; Maps, Sets, Dates, ArrayBuffers, Blobs, and plain data do (Errors clone on modern engines too). The working pattern for richer objects: send a description and rebuild - { tag: 'img', src: url } becomes an element on the far side through the receiver's own factory. This is a feature: the serialization boundary means a compromised frame cannot hand you a live object with methods - you get inert data you get to interpret.

Why is my iframe not receiving messages?

Four suspects in frequency order. Timing: the iframe has not loaded its listener yet - post from the parent on iframe load, or have the child announce readiness up first. Wrong handle: messages go to iframe.contentWindow, not the iframe element - posting at the element silently does nothing. Origin mismatch: targetOrigin is an exact string - a trailing slash, http vs https, or a port difference all fail delivery silently; log e.origin on the receiving side and compare byte for byte. Sandboxing: a sandboxed iframe without allow-scripts cannot run the listener at all. There is no delivery error channel - silence is the failure mode, so instrument both ends before debugging the protocol itself.

Related tools