skip to content

Cross-Window Messaging and Opener Risks

You will learn the sanctioned way to talk across origins — postMessage — and the two checks people skip that turn it into a vulnerability. Interviewers pair it with rel=noopener and reverse tabnabbing.

on this pageshow

questions

5

Your page embeds `<iframe id="widget" src="https://widget.example.com">` and you call `document.getElementById('widget').postMessage(data, 'https://widget.example.com')`, which throws a TypeError. What is the correct call, and what does each argument mean?

level: juniorimportance: must knowfreq 55%

answer

  1. the element is not the window
  2. postMessage lives on Window
  3. contentWindow, not contentDocument
  4. second argument names the receiver's origin
  5. no listener yet means lost message

basics

~20 s

An iframe element has no postMessage method; its window does. Call iframe.contentWindow.postMessage(data, 'https://widget.example.com') — the first argument is the copied payload, the second is the origin the frame must currently have or the browser silently drops the message.

solid answer

~40 s

`postMessage` is defined on `Window`, not on `HTMLIFrameElement`, so `iframeElement.postMessage` is undefined and calling it throws. The window of a framed document is `iframeElement.contentWindow`, so the correct call is `iframe.contentWindow.postMessage(data, 'https://widget.example.com')`. The first argument is the message, copied by the structured clone algorithm rather than shared by reference. The second argument, `targetOrigin`, is a safety check the browser performs at delivery time: if the frame's current origin does not match that string, the message is discarded with no error on either side. The framed page receives it as a `message` event on its own `window`, reading the payload from `event.data`. One practical trap: `contentWindow` exists as soon as the element does, long before the framed document has registered a listener, so a message sent too early is simply lost.

code

javascript · 14 lines
javascript
// Embedding page
const iframe = document.getElementById('widget');
const WIDGET_ORIGIN = 'https://widget.example.com';

window.addEventListener('message', (event) => {
  if (event.origin !== WIDGET_ORIGIN) return;
  if (event.data && event.data.type === 'ready') {
    // Only now is a listener guaranteed to exist inside the frame.
    event.source.postMessage({ type: 'setTheme', theme: 'dark' }, event.origin);
  }
});

// Inside https://widget.example.com, after attaching its own listener:
// window.parent.postMessage({ type: 'ready' }, 'https://parent.example');

go deeper

for a junior

Be able to write the call from memory: get the element, go through contentWindow, pass the payload and the exact target origin. Say plainly that an iframe element is not a window.

for a middle

Explain the delivery check the browser performs on targetOrigin, that the payload is structured-cloned rather than shared, and why a message sent before the framed page attaches its listener is simply lost.

for a senior

Show the handshake you would ship: the child announces readiness, the parent captures event.source and event.origin from that first message, and both sides use a typed message envelope so the protocol can evolve.

for a principal

Own the contract between embedder and widget — who publishes the message schema, how versions are negotiated at handshake time, and what a customer page is allowed to assume when your widget fails to load at all.

## The element and the window are different objects An `<iframe>` in your document is an `HTMLIFrameElement` — a node in *your* DOM tree, with attributes like `src`, `width` and `allow`. The document loaded inside it lives in a separate browsing context with its own global object. Cross-window messaging is defined on that global: the method is `Window.prototype.postMessage`. `HTMLIFrameElement` has no such method, so `iframeEl.postMessage(...)` is a plain `TypeError: ...postMessage is not a function`. Two properties bridge the element to what is inside it: - `iframeEl.contentWindow` — the framed context's `Window` (technically a `WindowProxy`). It is available **cross-origin**, but only a small set of members may be touched from another origin: `postMessage`, `closed`, `close`, `focus`, `blur`, `frames`, `length`, `top`, `parent`, `opener`, and writing `location`. - `iframeEl.contentDocument` — the framed `Document`. This is **null** whenever the frame is cross-origin, which is why reaching for it is the second most common version of this mistake. So the call is: ```js const iframe = document.getElementById('widget'); iframe.contentWindow.postMessage({ type: 'setTheme', theme: 'dark' }, 'https://widget.example.com'); ``` ## The arguments `postMessage(message, targetOrigin, transfer?)` — and there is an equivalent options form, `postMessage(message, { targetOrigin, transfer })`. `message` is not shared with the other side; it is serialized with the structured clone algorithm and a copy is delivered. Plain objects, arrays, `Date`, `Map`, `Set`, `ArrayBuffer` and typed arrays survive; functions, DOM nodes and class identity do not — attempting to send a function throws `DataCloneError`. `targetOrigin` is mandatory and is checked by the browser at delivery time. Pass the exact origin (scheme + host + port) the frame is expected to be on. If the frame is showing a different origin at that moment, the message is dropped silently — no exception for the sender, no event for the receiver. `'*'` disables the check entirely; `'/'` means "only deliver if the receiving window's origin equals mine". ## What the receiver sees The framed page listens on its own window: ```js window.addEventListener('message', (event) => { if (event.origin !== 'https://parent.example') return; console.log(event.data.type); }); ``` The event is a `MessageEvent` with `data` (the cloned payload), `origin` (the *sender's* origin, set by the browser and not forgeable by page script), `source` (a `WindowProxy` for the sender, usable to reply), and `ports` (any `MessagePort` objects that were transferred). Replying is symmetric: `event.source.postMessage(reply, event.origin)`, or from a framed page to its embedder, `window.parent.postMessage(reply, 'https://parent.example')`. ## Timing: the message is not queued for a future document `contentWindow` is non-null the instant the element is in the document, but at that point the frame may still be showing `about:blank`, and even once the real document is loading, its listener is not registered until its script runs. `postMessage` delivers to whatever document is in the frame *now*; there is no buffering for the next one. Two consequences: - Posting straight after `iframe.src = url` usually posts into `about:blank` and vanishes. - Posting on the parent's `iframe.onload` is better but still racy in practice, because load order between the frame's own scripts and the parent varies. The reliable pattern is a **child-initiated handshake**: the framed page posts `{type:'ready'}` to `window.parent` once its listener is attached, and the parent only starts sending after receiving it. That message also hands the parent a verified `event.source` and `event.origin` for replies. ## Common mistakes to name - Using `contentDocument` (null cross-origin) or the element itself instead of `contentWindow`. - Omitting `targetOrigin` — it is a required argument, so this is a `TypeError`, not a silent default. - Guessing at the origin string: `'https://widget.example.com/'` with a trailing path, or including a path segment, will not match. An origin is scheme, host and port only. - Assuming a delivered message is trustworthy on the receiving end. `targetOrigin` protects the sender's confidentiality; the receiver still has to check `event.origin` itself.

  • The parent posts to the frame immediately after setting `iframe.src` and the widget never receives anything — why?
    `postMessage` delivers to whatever document occupies the frame at that instant, and nothing is queued for a document that has not loaded yet. Right after setting `src` the frame is still `about:blank`, or the widget's script has not attached its listener. Have the widget post a `ready` message to `window.parent` once it is listening, and start sending only after that.
  • How does the framed page send a reply back to the embedder?
    Either `window.parent.postMessage(reply, 'https://parent.example')`, or inside the message handler `event.source.postMessage(reply, event.origin)`. The second form is preferable once you have validated `event.origin`, because it targets exactly the window that spoke to you rather than assuming the embedder is the direct parent.
  • What happens if the payload contains a function or a DOM node?
    `postMessage` throws a `DataCloneError`. The message is serialized with the structured clone algorithm, which copies data, not code or live objects: functions, DOM nodes, `Error` subclass identity and prototype chains do not survive. Send plain data — typically a `{type, payload}` object — and reconstruct behaviour on the receiving side.

saying these in an interview costs you the question

  • Thinking the iframe element itself has a postMessage method
  • Reaching for contentDocument on a cross-origin frame
  • Believing the message object is shared by reference
  • Treating targetOrigin as optional or as a hint
  • Posting right after setting src and expecting delivery

context

open as a page

What are the concrete risks of calling `window.postMessage(data, '*')` with a wildcard `targetOrigin`, and when is `'*'` genuinely acceptable?

level: middleimportance: must knowfreq 65%

basics

~20 s

A wildcard targetOrigin tells the browser to deliver the message no matter which origin the receiving window is on, so if that window navigated or was replaced, the payload lands in a stranger's page. Use '*' only for data that is safe to publish.

open as a page

A page runs `window.addEventListener('message', e => applyConfig(e.data))` to accept settings from an iframe it embeds. Why is that handler a security bug, and what must it check?

level: middleimportance: must knowfreq 70%

basics

~20 s

The window message event is a shared bus: any window holding a reference to yours — every embedded frame, your opener, a popup you opened — can post to it, so this handler applies configuration from strangers. Check event.origin against an exact allowlist, compare event.source to the expected window, and validate the payload's shape.

open as a page

You embed a third-party widget in an iframe and exchange dozens of messages per second in both directions. Why set up a `MessageChannel` and hand one port to the frame instead of calling `window.postMessage` with an origin check on every message?

level: seniorimportance: should knowfreq 35%

basics

~20 s

A window's message event is a shared bus every frame and opener can post to, so each message needs revalidation. A MessageChannel gives two entangled MessagePorts; after one verified handover, traffic flows on a private pipe only the two peers hold, with no per-message origin checks.

open as a page

A community site renders user-submitted links with `target="_blank"`. Explain reverse tabnabbing through `window.opener`, and what you would do about it given how browsers behave today.

level: seniorimportance: should knowfreq 45%

basics

~20 s

A page opened with target="_blank" gets a window.opener handle and, even cross-origin, may assign window.opener.location to redirect the tab the user came from to a phishing copy. Add rel="noopener" to user-submitted links, and pass 'noopener' to window.open, which modern anchor defaults do not cover.

open as a page