Modern web applications often require communication between multiple browsing contexts β windows, iframes, tabs, popups, or even workers.
The browser provides several APIs for this, each suited for different scenarios.
| API | Context | Cross-Origin Support | Direction | Typical Use Case |
|---|---|---|---|---|
| postMessage | Window β Iframe / Popup | β Yes | One-to-one | Secure cross-document messaging |
| MessageChannel | Any connected contexts | β Yes | Two-way | Dedicated bi-directional pipe |
| BroadcastChannel | Tabs / Frames / Workers (same origin) | β No | One-to-many | Messaging between tabs of same origin |
| SharedWorker | Tabs / Frames (same origin) | β No | Many-to-one | Shared state or caching logic |
| StorageEvent (localStorage) | Tabs (same origin) | β No | One-to-many | Simple tab sync via storage |
| Service Worker Messaging | All pages + worker | β Yes | Many-to-one | Background sync, push handling |
| Context | Reference | Example |
|---|---|---|
| Iframe | iframe.contentWindow |
const frame = document.getElementById('child').contentWindow; |
| Popup | window.open() |
const popup = window.open('https://child.com'); |
| Parent | window.parent |
From inside iframe |
| Opener | window.opener |
From popup |
| Named Frame | window.frames['frameName'] |
From parent page |
targetWindow.postMessage(message, targetOrigin, [transfer]);// Parent β Iframe
iframe.contentWindow.postMessage({ action: "fetchData" }, "https://child.com");
// Iframe β Parent
window.parent.postMessage({ user: "Alice" }, "https://parent.com");
// Listener
window.addEventListener("message", (event) => {
if (event.origin !== "https://child.com") return; // Always verify origin
console.log(event.data);
});- β
Verify
event.origin - β
Use specific
targetOrigin - π« Avoid
"*"unless local/dev only - β
Sanitize and validate
event.data - β Use HTTPS for all message contexts
Used for private communication between two contexts.
const { port1, port2 } = new MessageChannel();
iframe.contentWindow.postMessage("init", "https://child.com", [port2]);
port1.onmessage = e => console.log("Received:", e.data);Pros
- Fast and private (no global broadcast)
- Full duplex messaging
Cons
- Setup complexity
- Manual reference management
Allows all tabs, iframes, or workers from the same origin to communicate.
// Tab 1
const channel = new BroadcastChannel("notifications");
channel.postMessage({ event: "new-message", text: "Hello tabs!" });
// Tab 2
const channel2 = new BroadcastChannel("notifications");
channel2.onmessage = e => console.log("Received:", e.data);Pros
- Simple pub/sub pattern
- Auto-discovers all same-origin tabs
Cons
- Same-origin only
- No message persistence after reload
Security Tip:
All participants share trust β sanitize incoming data.
A SharedWorker can serve multiple tabs from the same origin simultaneously.
main.js
const worker = new SharedWorker("worker.js");
worker.port.start();
worker.port.postMessage("hello");
worker.port.onmessage = e => console.log(e.data);worker.js
onconnect = (e) => {
const port = e.ports[0];
port.onmessage = ev => port.postMessage(`Echo: ${ev.data}`);
};Pros
- Persistent shared logic or cache
- Handles multiple connections
Cons
- Same-origin only
- Limited browser support in iframes
Triggered when another tab updates localStorage.
// Tab A
localStorage.setItem("update", JSON.stringify({ msg: "Hi!" }));
// Tab B
window.addEventListener("storage", e => {
if (e.key === "update") console.log("Got:", JSON.parse(e.newValue));
});Pros
- Easiest to implement
- Works across tabs
Cons
- Same-origin only
- No structured clone (string-only)
- No event in same tab that triggered it
Allows persistent communication between pages and their background service worker.
// Client β SW
navigator.serviceWorker.controller.postMessage({ task: "sync" });
// SW β Client(s)
self.clients.matchAll().then(clients => {
clients.forEach(client => client.postMessage("Done"));
});Use Case: background sync, notifications, centralized data updates.
| API | Origin Restriction | Direction | Best For | Security Concern |
|---|---|---|---|---|
| postMessage | None (uses targetOrigin) | 1β1 | ParentβChild / Popup | Origin spoofing |
| MessageChannel | None (paired) | 1β1 | Private channel | Port leaks |
| BroadcastChannel | Same-origin | 1βN | Tabs of same app | Data trust |
| SharedWorker | Same-origin | Nβ1 | Shared logic | Worker compromise |
| StorageEvent | Same-origin | 1βN | Tab sync | No data validation |
| ServiceWorker | None (registered scope) | Nβ1 | Background processing | Message validation |
- Validate both origin and source for messages.
- Use structured clone safe data (no DOM elements/functions).
- Restrict iframe embedding using
X-Frame-Optionsor CSPframe-ancestors. - Donβt use
"*"intargetOrigin. - Apply sandbox attributes and proper
allowpermissions for iframes. - Avoid sensitive data in cross-origin messages.
| Use Case | Recommended API |
|---|---|
| Parent β Iframe | postMessage |
| Tabs (same origin) | BroadcastChannel |
| Popup β Opener | postMessage |
| Multiple tabs share a worker | SharedWorker |
| Background sync | ServiceWorker |
| Simple tab-to-tab state share | StorageEvent |
| Private channel | MessageChannel |
window.postMessage() provides a secure, browser-supported communication channel between:
- Windows and their child iframes
- Popups and their opener windows
- Tabs (through shared references like
window.opener,window.parent, orwindow.frames[]) - Service workers, extensions, and sandboxed contexts (via
MessageChannel)
This API is critical for cross-origin messaging and event-based data exchange.
Before sending or receiving messages, you need a window reference to communicate with.
| Context | How to Get Reference | Example |
|---|---|---|
| Iframe (child) | Use iframe.contentWindow |
const child = document.getElementById('frame').contentWindow; |
| Popup / New Tab | Use window.open() result |
const popup = window.open('https://child.com'); |
| Parent Window | From inside iframe | const parent = window.parent; |
| Specific Frame | From parent page | const frame = window.frames['frameName']; |
| Opener Window | From popup | const opener = window.opener; |
You can only send messages to windows you can reference β thereβs no βbroadcastβ to all tabs.
targetWindow.postMessage(message, targetOrigin, [transfer]);| Parameter | Description |
|---|---|
message |
Any serializable data (string, object, array, etc.) |
targetOrigin |
The expected destination origin (e.g., "https://child.com"). Use "*" only in development. |
transfer |
Optional: list of transferable objects (e.g., ArrayBuffer, MessagePort). |
const iframe = document.getElementById("child");
iframe.contentWindow.postMessage({ action: "getUser" }, "https://child.com");window.parent.postMessage({ user: "Alice" }, "https://parent.com");window.addEventListener("message", (event) => {
if (event.origin !== "https://child.com") return; // β
Always verify sender
console.log("Message from child:", event.data);
});| Property | Description |
|---|---|
event.data |
The actual message payload |
event.origin |
The senderβs origin (scheme + host + port) |
event.source |
Reference to the sender window |
event.ports |
Array of transferred MessagePort objects |
| Pattern | Example | Description |
|---|---|---|
| Parent β Iframe | window.parent, iframe.contentWindow |
Cross-origin or same-origin frame messaging |
| Popup β Opener | window.opener |
When a site opens a new tab/popup |
| Iframe β Iframe (via parent) | Relay messages through parent window | |
| Two independent tabs | Use BroadcastChannel or ServiceWorker |
postMessage alone canβt connect tabs |
β
Always specify targetOrigin β Avoid "*" unless absolutely necessary.
β
Always verify event.origin β Confirm sender domain matches expected origin.
β
Avoid sending sensitive data unless over HTTPS and verified recipient.
β
Sanitize input β Never trust messages without validation.
β
Restrict iframe permissions using attributes like sandbox, allow-same-origin, allow-scripts.
β
Disable or limit third-party framing with CSP headers (frame-ancestors).
| Risk | Description | Mitigation |
|---|---|---|
| Origin Spoofing | Attacker sends fake messages from malicious origin | Validate event.origin before acting |
| XSS Relay | Malicious script in child sends payload to parent | Sanitize event.data |
| Data Leak | Using "*" exposes data to any site |
Use explicit targetOrigin |
| Logic Abuse | Message handler performs unintended actions | Implement action whitelists and strict schema checks |
const { port1, port2 } = new MessageChannel();
iframe.contentWindow.postMessage("init", "https://child.com", [port2]);
port1.onmessage = e => console.log("From iframe:", e.data);This enables bi-directional communication channels beyond basic postMessage.
| Tool / Method | Use |
|---|---|
console.log(event.origin, event.data) |
Log inbound messages |
| Browser DevTools β Frames | Inspect window/frame hierarchy |
| Network Tab | postMessage does not show in network logs (client-only) |
| CSP + Sandbox Headers | Restrict iframes and external message access |
| Operation | Requirement | Security Check | Example |
|---|---|---|---|
| Send message | Reference to target window | targetOrigin verification |
target.postMessage(data, origin) |
| Receive message | addEventListener('message') |
event.origin validation |
if (event.origin === 'https://trust.com') |
| Bidirectional channel | MessageChannel |
Validate message source | const {port1, port2} = new MessageChannel() |
- Use explicit origins β never
"*"in production. - Validate both
originandsource. - Sanitize
dataif user-controlled. - Donβt expose privileged APIs directly through message handlers.
- Use structured clone safe data (no functions, DOM nodes).
- Enable
sandboxandCSPfor framed pages.