postMessage and Origin Checks
Cross-origin iframe and worker messaging with postMessage — targetOrigin, event.origin checks, and structured clone pitfalls.
- javascript
- postmessage-and
Browsers isolate origins. When a parent page talks to an iframe on another origin (SSO, payments, embeds), the bridge is window.postMessage. Skip origin checks and any site can send you forged messages.
Sending
// parent
const iframe = document.querySelector('iframe');
iframe.contentWindow.postMessage(
{ type: 'INIT', token: sessionToken },
'https://widget.example.com', // targetOrigin — NEVER '*' with secrets
);
| Argument | Role |
|---|---|
message |
Cloned via structured clone (not shared) |
targetOrigin |
Receiver must match; '*' allows any — dangerous for sensitive data |
transfer |
Optional transferable objects (ArrayBuffers) |
Receiving (the part people skip)
window.addEventListener('message', (event) => {
// 1) Who sent this?
if (event.origin !== 'https://parent.example.com') return;
// 2) Optional: which window?
// if (event.source !== expectedWindow) return;
const data = event.data;
if (!data || typeof data !== 'object') return;
if (data.type === 'INIT') {
start(data.token);
}
});
Always validate event.origin (and ideally a message schema). event.data can be anything from any other frame that got a reference to your window.
Bidirectional handshake pattern
// child ready
parent.postMessage({ type: 'READY' }, 'https://app.example.com');
// parent
window.addEventListener('message', (e) => {
if (e.origin !== 'https://widget.example.com') return;
if (e.data?.type === 'READY') {
e.source.postMessage({ type: 'CONFIG', theme: 'dark' }, e.origin);
}
});
Use the known origin string on send; use e.origin only after you’ve verified it equals an allowlist entry.
Workers and same-origin frames
Dedicated workers use worker.postMessage / self.onmessage — no origin (same agent cluster). Shared workers and service workers have their own rules; still treat message data as untrusted input if multiple clients exist.
Structured clone limits
// OK: plain objects, arrays, Date, Map, Set, ArrayBuffer, ...
// Not OK: functions, DOM nodes, symbols as values in some cases, class instances with methods
iframe.contentWindow.postMessage(() => {}, 'https://x.test'); // DataCloneError
If you need functions, you don’t — redesign as message protocol + IDs.
Security checklist
- Explicit
targetOriginwhen sending secrets - Allowlist
event.originon receive - Validate shape / type discriminants
- Don’t eval message strings
- Prefer short-lived tokens over long-lived secrets in messages
- CSP and frame-ancestors still matter — postMessage isn’t a substitute for framing policy
Debug tip
Log event.origin and event.source when integrating; forged messages in production often look like “random” client bugs until you filter origin.
Interview answer (out loud)
“postMessage sends a structured-clone of data to another window. I always set a specific targetOrigin when posting sensitive data, and on message I allowlist event.origin and validate the payload shape. ‘*’ and unchecked listeners are classic XSS/data-leak patterns with iframes.”
MessageChannel for capability transfer
const { port1, port2 } = new MessageChannel();
iframe.contentWindow.postMessage({ type: 'PORT' }, 'https://widget.example', [port2]);
// further chat on port1 without re-checking window origin each time —
// still establish trust on first handoff
Ports are transferable. Useful for long-lived widgets after an authenticated handshake.
Wildcard origin audits
Search the codebase for postMessage( and '\\*' as a security review checklist item. Same for addEventListener('message' without origin checks.
Further reading
Related
Related guides
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.
- Async Iteration and for await...ofAsync iterables, for await...of, and streaming data — how async iterators differ from Promise.all and when to use each.