Windows, iframes & postMessage
Learn how popups, iframes, sandboxing, postMessage, origin checks, and clickjacking defenses let browser windows communicate safely.
- 01Open windows deliberatelyUse popups only from user gestures, handle blocked windows, and understand noopener.
- 02Frame safelyExplain same-origin iframe limits, sandbox tokens, and why
allow-scriptsplusallow-same-originis dangerous for same-origin content. - 03Validate messagesSend structured-cloned data with
postMessage, then checkevent.origin,event.source, and data shape before acting.
Several windows, one careful page
A web app is not always one document in one tab. It may open a popup for sign-in, embed a payment widget in an iframe, preview untrusted content in a sandbox, or receive a message from a map, video player, or account portal. Those pieces are all browsing contexts: separate places where a document lives.
The important idea is simple: a reference is not permission. Your page may hold a reference to another window, but the browser still decides what can be read, changed, or messaged. That decision starts with the same-origin policy from the CORS lesson: scheme, host, and port must match before one document can freely inspect another.
| Feature | Popup | Iframe |
|---|---|---|
| Created by | window.open() or a link with target="_blank" | An <iframe> element in the page |
| Where it appears | A separate tab or window in most browsers | Inside the current page’s layout |
| Common use | OAuth sign-in, print previews, optional tools | Embedded maps, payments, videos, widgets, isolated demos |
| Safety habit | Open from a click, handle null, use noopener | Use sandbox, a narrow src, and message validation |
An iframe lets one page appear inside another. If the framed page belongs to the same origin, it is like another room in your own house. If it belongs to a different origin, you can see that it exists, but you cannot reach through the window and move the furniture.
- In real life: A window in your own house
- In JavaScript: A same-origin iframe you may read and change
- In real life: A window into a neighbor's house
- In JavaScript: A cross-origin iframe you can see but cannot reach into
- In real life: Knocking and passing a note
- In JavaScript: Using
postMessageinstead of reading private state
Where the analogy stops: A real neighbor can invite you in. Browser permission is stricter: the origin, sandbox tokens, and headers decide what code can do.
This lesson builds the safe habits: open popups only when the user asked, treat cross-origin frames as opaque, send messages with a narrow target, validate messages on arrival, and protect sensitive pages from being framed in the first place.
window.open and popups
INTERACTIVEA popup is a top-level browsing context: usually a new tab or window. JavaScript opens one with window.open(url, target, features). In most browsers, a popup is allowed when that call happens directly during a user gesture, such as a button click. Calls from timers, page load, or background code are often blocked, and window.open returns null.
The safest popup code has a boring shape: call it from a click, check for null, do only what the browser allows, then clean up. The lab opens about:blank, which inherits the opener’s origin, writes a tiny document, reads closed, and closes it. It also shows that noopener cuts the opener link and returns null.
button.addEventListener("click", () => { const popup = window.open("about:blank", "lessonPopup", "width=360,height=240"); if (!popup) return "blocked"; popup.document.write("<h1>Hello from the opener</h1>"); console.log(popup.closed); popup.close();}); const safePopup = window.open("about:blank", "_blank", "noopener");console.log(safePopup);Click a button to open a popup from a user gesture.
Browsers usually allow this because the call happens directly inside a click handler. Code that tries later, without a user gesture, is often blocked.
For links that open a new tab, write target="_blank" rel="noopener". Modern browsers often default target="_blank" to noopener, but being explicit documents the security reason: the new page should not control its opener.
Iframes and same-origin access
An iframe embeds another document inside the current page. Same-origin iframes are powerful: the parent can read their DOM, call functions, and share origin-scoped storage. Cross-origin iframes are intentionally narrow. The parent can keep a window reference, but most reads throw a security error.
| Relationship | What JavaScript can usually do | What it cannot do |
|---|---|---|
| Same-origin iframe | Read and change its DOM, call functions, read storage for that origin | Bypass sandbox tokens if the sandbox still blocks an action |
| Cross-origin iframe | Hold a window reference, call postMessage, focus, blur, close, read closed, length, and assign location in limited ways | Read its DOM, cookies, storage, URL path, variables, or most properties |
Sandboxed without allow-same-origin | Run only the permissions named by sandbox tokens; event.origin becomes "null" | Behave as the parent’s normal same-origin document |
That narrow list is still useful. Cross-origin references can usually use postMessage, focus, blur, close, read simple properties such as closed, length, and frames, and assign location in limited ways. They cannot read the other page’s DOM, cookies, storage, variables, or full URL.
The browser does not care that two pages are both on your laptop during development. If the scheme, host, or port differs, they are cross-origin. The URL objects lesson is helpful for reading those pieces precisely.
postMessage and origin checks
STEP THROUGHWhen two windows are not allowed to read each other directly, they can still cooperate by sending messages. The sender calls otherWindow.postMessage(data, targetOrigin). Later, the receiver gets a message event. That event has data, origin, and source.
Anyone can push a note through a mail slot. Before you unlock a door, you read the return address and check who handed it over. targetOrigin is like writing the recipient’s name on the envelope: if the frame navigated somewhere else, the browser refuses delivery.
- In real life: The note
- In JavaScript:
event.data, copied with structured clone - In real life: The return address
- In JavaScript:
event.origin - In real life: The person who handed it over
- In JavaScript:
event.source - In real life: Writing the recipient's name
- In JavaScript:
targetOrigin
Where the analogy stops: A paper note can be forged. Browser origins are supplied by the browser, but you still must validate the sender and the data shape before acting.
Opaque sandboxed frames are the awkward case. Without allow-same-origin, their origin is exposed to message handlers as the string "null". You cannot name that as a normal URL origin when sending, so * is required for delivery. That makes the receiving checks even more important.
const frame = document.querySelector("iframe");const trustedWindow = frame.contentWindow; window.addEventListener("message", (event) => { if (event.origin !== "null") return; if (event.source !== trustedWindow) return; show("accepted", event.data);}); trustedWindow.postMessage({ kind: "ping" }, "*");Use the controls to send messages.
The trusted frame is sandboxed without allow-same-origin, so its message origin is the string null. A specific page origin does not match that opaque origin; * is needed for delivery, then the receiver checks origin, source, and data.
The live lab uses two sandboxed srcdoc frames. Both have origin "null", but only one is the stored trusted contentWindow. The stranger can copy the data shape, but it cannot pass the event.source check.
Choose the trusted frame or the stranger, then step through the origin and source checks.
script
const trustedFrame = "lesson-frame"; function receiveMessage(event) { const originOK = event.origin === trustedOrigin; const sourceOK = event.source === trustedFrame; if (!originOK || !sourceOK) return "rejected"; return "accepted: " + event.data.kind;} receiveMessage(incomingEvent);Message data is structured-cloned: plain objects are copied into the other window, not shared. Functions and DOM nodes cannot be cloned. The next lesson goes deep on structured cloning, transfer, and channels; here the key habit is validation before action.
- Checks
event.originandevent.sourcebefore updating state - Sends a secret token with
targetOrigin: "*" - Accepts only objects with
type: "cart-ready"and a numericid - Runs whatever command name arrives in
event.data.action - Uses
child.postMessage(message, "https://pay.example")for a payment frame - Accepts every message whose origin is
"null"
Sort each realistic snippet. Look for target origin, origin checks, source checks, and data validation.
Sandboxed frames
INTERACTIVEThe sandbox attribute starts an iframe with many powers removed. Then tokens add specific powers back. A bare sandbox blocks scripts, forms, popups, modals, and same-origin treatment. Tokens such as allow-scripts, allow-forms, allow-popups, allow-modals, and allow-same-origin each open one door.
// The parent rebuilds the iframe with selected tokens.frame.sandbox = "allow-scripts allow-forms";frame.srcdoc = predefinedHtml; // Inside predefinedHtml:parent.postMessage({ scriptRan: true, origin: self.origin, canSubmitForms: sandbox.contains("allow-forms"), canOpenPopups: sandbox.contains("allow-popups"), canShowModals: sandbox.contains("allow-modals")}, "*");nullStart locked down, then add only the powers the frame truly needs. Without allow-scripts, this demo frame cannot report from inside at all.
Be very cautious with sandbox="allow-scripts allow-same-origin" on same-origin content. Scripts can run, and the frame is treated as same-origin, which can let it remove or work around the sandbox you expected to enforce. For untrusted examples, prefer an opaque origin and communicate with validated messages.
This is why the CORS sandbox demo and the postMessage lab both use predefined srcdoc and sandbox="allow-scripts". The code can run, but the frame stays opaque and all communication goes through a small message protocol.
Clickjacking: when the visual page lies
INTERACTIVEClickjacking, also called UI redressing, is not about reading another page. It is about tricking a user into clicking it. The attacker frames a sensitive page, puts a decoy on top, and lines up the real button underneath the fake one.
Imagine a transparent sheet laid over a real button. The label on the sheet says Play, but your finger presses the Transfer money button underneath. That mismatch is the danger.
- In real life: A transparent sheet
- In JavaScript: A decoy layer above an iframe
- In real life: The button underneath
- In JavaScript: The real framed action
- In real life: Thinking you clicked Play
- In JavaScript: The user sees the decoy
- In real life: Actually clicking Transfer
- In JavaScript: The browser sends the click to the hidden target
Where the analogy stops: Real attacks tune pointer events, opacity, and layout. This lesson uses a visible slider so you can inspect the trick safely.
<iframe title="fake bank" sandbox srcdoc="...bank button..."></iframe><div class="decoy"> <p>Looks like a video player.</p> <button>Play</button></div> /* Lower the decoy opacity to reveal the hidden frame underneath. */Video preview
The pointer is over the decoy, but another page can be underneath.At high opacity you see a harmless decoy. Lower it and the framed fake bank underneath appears. Real defenses are response headers from the sensitive site, not a script inside the victim page.
| Defense | Set by | What it means |
|---|---|---|
X-Frame-Options: DENY | Server | No site may frame this page. |
X-Frame-Options: SAMEORIGIN | Server | Only pages from the same origin may frame it. |
Content-Security-Policy: frame-ancestors 'self' https://partner.example | Server | Modern, flexible allow-list for who may embed the page. |
| Frame-busting script | Page JavaScript | Unreliable because scripts can be blocked, raced, or sandboxed. |
This public lesson page does not send those protective headers because there is no sensitive action to protect. An admin site, account settings page, or payment approval page should. In our stack, the admin site sends X-Frame-Options: DENY; public lessons do not need to block learners from embedding harmless content.
Where you will use this
These APIs show up in ordinary product work:
- OAuth sign-in. Open a provider popup from a click, use
noopenerwhen appropriate, and receive a final success message from a controlled redirect page. - Payment widgets. Embed a cross-origin iframe, send it setup data with a specific
targetOrigin, and accept only messages from its known origin and source. - CMS previews. Render preview HTML in a sandboxed iframe so it cannot mutate the editor page. Use messages for height, navigation, or selection events.
- Interactive docs. Run risky demos in sandboxed
srcdocframes and report results back through validated messages.
- Store the expected popup or
contentWindowreference. - Send with a specific
targetOriginunless the target is intentionally opaque. - Check
event.origin,event.source, and the data shape. - Ignore unknown messages. Do not throw from a global message handler.
- Protect sensitive pages with
X-Frame-Optionsor CSPframe-ancestors.
Common misconceptions
"I have a window reference, so I can read it."
Not across origins. A reference lets you use a few safe operations, especially postMessage, but most reads are blocked.
"Checking origin is always enough."
Opaque sandboxed frames can all report "null". Same-origin pages can also contain multiple frames. Check event.source too.
"targetOrigin: * means insecure every time."
It is sometimes necessary for opaque origins, but do not send secrets with it, and validate on receipt.
"Sandbox makes any iframe safe."
The tokens matter. In particular, allow-scripts plus allow-same-origin can undermine a sandbox for same-origin content.
"Frame-busting JavaScript prevents clickjacking."
It is unreliable. Use server headers: X-Frame-Options or CSP frame-ancestors.
Practice: safe windows and frames
5 EXERCISESA parent page needs to send a non-secret ping to a sandboxed srcdoc frame whose origin is "null". What targetOrigin is required?
Use * for delivery to an opaque sandboxed frame, and keep the payload non-secret. The receiver still checks event.origin, event.source, and the data shape.
Read the code and predict the printed word.
const trustedOrigin = "null";
const trustedSource = "frame-a";
const event = { origin: "null", source: "frame-b" };
console.log(event.origin === trustedOrigin && event.source === trustedSource ? "accept" : "reject");It prints reject. The origin is "null", but the source is frame-b, not the trusted frame-a.
What two values print, in order?
const popup = { closed: false, close() { this.closed = true; } };
console.log(popup.closed);
popup.close();
console.log(popup.closed);The program prints false and then true. Real popup references expose a closed property too.
Name one reliable header or directive that prevents a sensitive page from being framed by attackers.
Good answers include X-Frame-Options or CSP frame-ancestors. Scripts alone are not reliable clickjacking protection.
Find the security problem in the handler. Then type the risky sandbox token pair from this lesson.
window.addEventListener("message", (event) => {
if (event.origin === "null") {
runCommand(event.data.action);
}
});window.addEventListener("message", (event) => {
if (event.origin !== "null") return;
if (event.source !== trustedFrame.contentWindow) return;
if (!event.data || event.data.type !== "preview-ready") return;
markPreviewReady();
});The original trusts every opaque-origin message and runs an arbitrary action name. The safer version checks origin, source, and a small data shape before doing one known action.
Quiz: check your understanding
7 QUESTIONSYour score counts first tries. Read the explanations for wrong answers; they are written like mini code reviews.
Question 1 of 7What should a popup-opening function check first after
window.open?Choose an answer to see the explanation.
Question 2 of 7What does this message validator print for the spoofed frame?
Read the code, then predictconst trustedOrigin = "null"; const trustedSource = "lesson-frame"; const event = { origin: "null", source: "stranger-frame" }; console.log(event.origin === trustedOrigin && event.source === trustedSource ? "accept" : "reject");Choose an answer to see the explanation.
Question 3 of 7Why is
targetOrigin: "*"sometimes necessary for sandboxedsrcdocframes?Choose an answer to see the explanation.
Question 4 of 7Which cross-origin window operation is generally allowed?
Choose an answer to see the explanation.
Question 5 of 7What is risky about
sandbox="allow-scripts allow-same-origin"on same-origin content?Choose an answer to see the explanation.
Question 6 of 7What does this popup model print?
Read the code, then predictconst popup = { closed: false, close() { this.closed = true; } }; console.log(popup.closed); popup.close(); console.log(popup.closed);Choose an answer to see the explanation.
Question 7 of 7Which is a reliable clickjacking defense?
Choose an answer to see the explanation.
Key takeaways
- Popups should open from user gestures; handle
null,closed, andnoopener. - Same-origin iframes are readable; cross-origin iframe references are narrow but can use
postMessage. postMessagedata is structured-cloned and delivered later as amessageevent.- Validate
event.origin,event.source, and the data shape before acting. - Sandbox tokens add back powers one by one; avoid risky combinations for same-origin untrusted content.
- Clickjacking is stopped by server headers such as
X-Frame-Optionsand CSPframe-ancestors.
One-liner.
Windows and frames can cooperate safely when the browser limits access and your code validates every message before it acts.
Up next: Messaging & structured cloning.