Cross-window communication
Open two tabs. One is your bank, the other is some random site you clicked into from a forum. Should the random site be able to reach into the bank tab and read your account number? Obviously not. The browser enforces that wall for you, and the rule that defines the wall is called the Same Origin policy.
The scenario the policy guards against is concrete: you have a page from maya-vance.com open, and another from gmail.com. A script on maya-vance.com must not be able to read your inbox on gmail.com. The policy exists to stop exactly that kind of information theft.
Same Origin
Two URLs have the same origin when three things match: the protocol, the domain, and the port. All three. Miss one and it’s a different origin.
These URLs all share one origin — the extra path doesn’t matter:
http://site.comhttp://site.com/http://site.com/my/page.html
These do not match http://site.com:
http://www.site.com— different domain (www.is its own subdomain)http://site.org— different domain (.orgvs.com)https://site.com— different protocolhttp://site.com:8080— different port
What the policy actually grants and forbids:
- If you hold a reference to another window — a popup you made with
window.open, or a window living inside an<iframe>— and that window is same origin, you get full access. Its variables, itsdocument, everything. - If it’s a different origin, that access is cut off. You can’t read its variables or touch its
document. The single exception islocation: you’re allowed to write to it (redirecting the user), but you can’t read it. Writing but not reading means you can send someone somewhere without learning where they currently are — no leak.
In action: iframe
An <iframe> embeds a whole separate window inside your page. It carries its own document and its own window object, independent of the parent.
Two properties reach into it:
iframe.contentWindow— thewindowobject inside the frame.iframe.contentDocument— thedocumentinside, a shorthand foriframe.contentWindow.document.
Every time you reach through one of these, the browser checks the origin of the framed page. Mismatch, and the access throws — except for writing location, which stays legal.
Here’s a cross-origin frame refusing almost everything:
<iframe src="https://example.com" id="iframe"></iframe>
<script>
iframe.onload = function() {
// we can grab the reference to the inner window
let iframeWindow = iframe.contentWindow; // OK
try {
// ...but not the document inside it
let doc = iframe.contentDocument; // ERROR
} catch(e) {
alert(e); // Security Error (another origin)
}
// we also can't READ the URL of the page in the iframe
try {
// Can't read href from the Location object
let href = iframe.contentWindow.location.href; // ERROR
} catch(e) {
alert(e); // Security Error
}
// ...but we CAN write location (loading something else into the iframe)!
iframe.contentWindow.location = '/'; // OK
iframe.onload = null; // drop the handler so it doesn't fire again after the redirect
};
</script>
Only two operations survive: grabbing iframe.contentWindow, and writing location. Everything else throws a security error.
When the <iframe> is same origin, the wall disappears and you can do whatever you like:
<!-- iframe from the same site -->
<iframe src="/" id="iframe"></iframe>
<script>
iframe.onload = function() {
// full access
iframe.contentDocument.body.prepend("Hello, world!");
};
</script>
Because the frame below is same origin with the page hosting it, the outer script can reach straight through iframe.contentDocument and edit the frame’s DOM. Click the button and watch a same-origin page rewrite the contents of a frame it doesn’t own:
Windows on subdomains: document.domain
Different domains mean different origins, by definition. But there’s a special case for sibling subdomains.
Suppose you run maya.site.com, raj.site.com, and site.com. They differ, yet they share the second-level domain site.com. You can tell the browser to overlook the subdomain difference and treat them as one origin for cross-window purposes.
Each participating window runs one line:
document.domain = 'site.com';
That’s the whole trick. Once every window has set the same value, they can talk freely. It only works when they already share that second-level domain — you can’t set document.domain to just anything.
Iframe: wrong document pitfall
This one isn’t about cross-origin security — it bites even on same-origin frames — but it trips people up constantly.
The moment an <iframe> element exists, it already carries a document. The problem: that initial document is not the one that ends up loading from src. The browser creates a throwaway blank document first, then replaces it once the real page arrives.
So anything you do to that first document gets thrown away with it:
<iframe src="/" id="iframe"></iframe>
<script>
let oldDoc = iframe.contentDocument;
iframe.onload = function() {
let newDoc = iframe.contentDocument;
// the loaded document is a different object from the initial one!
alert(oldDoc == newDoc); // false
};
</script>
Never touch the document of a not-yet-loaded iframe — it’s the wrong document, and event handlers you attach to it are silently discarded.
So when is the right document actually there?
The safe answer is iframe.onload: by the time it fires, the real document is in place. The downside is that onload waits for the whole frame plus every resource to finish.
If you need to catch the swap sooner, poll with setInterval and watch for the document reference to change:
<iframe src="/" id="iframe"></iframe>
<script>
let oldDoc = iframe.contentDocument;
// every 100 ms, check whether the document has been replaced
let timer = setInterval(() => {
let newDoc = iframe.contentDocument;
if (newDoc == oldDoc) return;
alert("New document is here!");
clearInterval(timer); // got it, stop polling
}, 100);
</script>
Collection: window.frames
iframe.contentWindow isn’t the only handle on a frame’s window. There’s also the named collection window.frames:
- By index:
window.frames[0]— the window of the first frame in the document. - By name:
window.frames.iframeName— the window of the frame whosename="iframeName".
They point at the same object:
<iframe src="/" style="height:80px" name="win" id="iframe"></iframe>
<script>
alert(iframe.contentWindow == frames[0]); // true
alert(iframe.contentWindow == frames.win); // true
</script>
Frames can nest — an iframe can hold iframes of its own — and their window objects form a tree. Three properties let you climb it:
window.frames— the collection of direct child windows.window.parent— the window one level up.window.top— the window at the very top of the tree.
Which gives the identity:
window.frames[0].parent === window; // true
top is handy for detecting whether your page is running standalone or embedded in someone else’s frame:
if (window == top) { // is the current window the topmost one?
alert('The script is in the topmost window, not in a frame');
} else {
alert('The script runs in a frame!');
}
The “sandbox” iframe attribute
The sandbox attribute lets you strip capabilities from an <iframe> so it can host untrusted content without doing damage. It “sandboxes” the frame — forcing it into a foreign origin and switching off risky features.
There’s a strict default set of restrictions when you write <iframe sandbox src="..."> with no value. From there you relax it by listing the permissions you want to grant back, space-separated: <iframe sandbox="allow-forms allow-popups">.
So the mental model is inverted from what you might expect: empty sandbox locks everything down, and each token you add lifts one specific restriction.
Some of the tokens:
allow-same-origin — By default sandbox forces the “different origin” treatment on the frame, so even a frame whose src points at your own site is treated as foreign, with all the script restrictions that implies. This token removes that forcing, restoring the frame’s real origin.
allow-top-navigation — Lets the frame change parent.location (navigate the outer page).
allow-forms — Lets the frame submit forms.
allow-scripts — Lets the frame run its own scripts.
allow-popups — Lets the frame open popups via window.open.
See the iframe reference on MDN for the complete list.
The demo below loads the same little page — some JavaScript that tries to announce itself, plus a form — into a frame twice. Toggle the sandbox on and off. With the bare default set (<iframe sandbox src="...">), the script never runs and the form is inert; that is how harsh the default really is.
Cross-window messaging
postMessage is the sanctioned way for windows to exchange data regardless of their origins. It’s the door through the Same Origin wall.
A page on maya-vance.com can hand data to a page on gmail.com — but only because both sides opted in by calling the right functions. Nobody eavesdrops; both parties have to participate. That’s what keeps it safe.
The API has two halves: the sender’s postMessage call, and the receiver’s message event handler.
postMessage
The sender calls postMessage on the receiving window. To message the window win, you call win.postMessage(data, targetOrigin).
data — Whatever you want to send. It can be any object; the browser copies it using the structured clone algorithm, so you’re not sharing a live reference — the receiver gets its own copy.
targetOrigin — Restricts delivery to a window that currently holds a document from that exact origin. A safety gate.
Why the gate matters: remember you can’t read a cross-origin window’s location. So you genuinely don’t know which site is loaded in the target right now — the user might have navigated away, and your window has no way to tell. targetOrigin says “only deliver this if the receiver is still on the origin I expect.” When the payload is sensitive, that check is the difference between handing your secret to the right site and handing it to whatever page slipped into that window.
Here win only gets the message if it’s showing a document from http://example.com:
<iframe src="http://example.com" name="example">
<script>
let win = window.frames.example;
win.postMessage("message", "http://example.com");
</script>
Drop the check by passing * — deliver to whoever is there. Only do this when the data isn’t sensitive:
<iframe src="http://example.com" name="example">
<script>
let win = window.frames.example;
win.postMessage("message", "*");
</script>
data,
targetOrigin
)
(or targetOrigin is *)
fires with
{data, origin, source}
onmessage
On the receiving side, listen for the message event. It fires when someone calls postMessage at your window and the targetOrigin check passes.
The event object carries three properties worth knowing:
data — The payload from postMessage.
origin — The origin of the sender, for example http://example.com. This one you can trust: the browser fills it in, the sender can’t fake it. Always check it before trusting the data.
source — A reference to the sender’s window. Call source.postMessage(...) to reply immediately.
One gotcha: you must register the handler with addEventListener. The shorthand window.onmessage = ... does not work for this event.
window.addEventListener("message", function(event) {
if (event.origin != 'http://example.com') {
// came from a domain we don't trust — ignore it
return;
}
alert( "received: " + event.data );
// reply with event.source.postMessage(...) if you like
});
The full working example is below. The frame runs a message listener that echoes whatever it receives; the outer page sends through frame.contentWindow.postMessage and prints the frame’s reply. Type something and hit Send — the round trip is real postMessage traffic.
Summary
To reach into another window’s methods or content, you first need a reference to it.
For popups:
- From the opener:
window.openopens a new window and returns a reference to it. - From the popup:
window.openerpoints back at the window that opened it.
For iframes, navigate the window tree with:
window.frames— the collection of nested child windows.window.parentandwindow.top— the parent window and the topmost one.iframe.contentWindow— the window inside a given<iframe>element.
When windows share an origin (protocol, host, and port), they can do anything to each other.
Otherwise, only two actions are open:
- Write the other window’s
location(redirect it — write-only, no reading back). - Post a message to it.
Two ways to relax the origin barrier:
- Windows on the same second-level domain, like
a.site.comandb.site.com, become “same origin” once both rundocument.domain='site.com'. - An
<iframe sandbox>is forced into a foreign-origin state unlessallow-same-originis present. This lets you run untrusted code in a frame even when it’s served from your own site.
And postMessage connects windows across any origins at all:
-
The sender calls
targetWin.postMessage(data, targetOrigin). -
Unless
targetOriginis'*', the browser confirmstargetWin’s origin matchestargetOriginbefore delivering. -
On a match,
targetWinreceives amessageevent carrying:origin— the sender’s origin, e.g.http://my.site.com.source— a reference back to the sender’s window.data— the copied payload, any structured-cloneable object.
Register the handler with
addEventListener("message", ...), and validateoriginbefore you trust anything.