ESC

Type to search the knowledge base.

postMessage and Origin Checks

Cross-origin iframe and worker messaging with postMessage — targetOrigin, event.origin checks, and structured clone pitfalls.

advanced3 min read
  • 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

  1. Explicit targetOrigin when sending secrets
  2. Allowlist event.origin on receive
  3. Validate shape / type discriminants
  4. Don’t eval message strings
  5. Prefer short-lived tokens over long-lived secrets in messages
  6. 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 guides