window.postMessage()
The Complete Guide to Cross-Origin Communication and Security
What is postMessage?
The window.postMessage() method safely enables cross-origin communication between Window objects—for example, between a page and a pop-up it spawned, or between a page and an iframe embedded within it.
Basic Syntax
targetWindow.postMessage(message, targetOrigin, [transfer]);
message
Data to be sent to the target window. The data is serialized using the structured clone algorithm.
targetOrigin
Specifies what the origin of targetWindow must be for the event to be dispatched. Use "*" for no preference (dangerous!).
transfer (optional)
A sequence of transferable objects that are transferred with the message.
Receiving Messages
JavaScriptwindow.addEventListener("message", (event) => {
// CRITICAL: Always verify the origin
if (event.origin !== "https://trusted-site.com") {
return;
}
// Access message data
console.log(event.data);
console.log(event.origin);
console.log(event.source);
});
Key Properties of MessageEvent
| Property | Description | Security Impact |
|---|---|---|
event.data |
The message sent by the sender | Untrusted user input |
event.origin |
The origin of the sender window | Must be validated |
event.source |
A reference to the window that sent the message | Can be used for replies |
Security Fundamentals
postMessage security relies on two critical checks: sender validation when receiving messages, and target specification when sending messages.
❌ Missing Origin Check
// VULNERABLE CODE
window.addEventListener("message", (event) => {
// No origin validation!
eval(event.data); // Arbitrary code execution
});
Impact: Any website can send malicious payloads
✓ Proper Origin Check
// SECURE CODE
window.addEventListener("message", (event) => {
if (event.origin !== "https://trusted.com") {
return;
}
processData(event.data);
});
Result: Only messages from trusted origin are processed
The Two Security Rules
- When Receiving: Always validate
event.originbefore processingevent.data - When Sending: Always specify exact target origin, never use
"*"for sensitive data
Common Vulnerabilities
1. XSS via Missing Origin Validation
Vulnerablewindow.addEventListener("message", function(e) {
document.getElementById("output").innerHTML = e.data; // XSS!
});
Attack scenario:
// Attacker's page at evil.com
targetWindow.postMessage("<img src=x onerror=alert(document.cookie)>", "*");
2. Authentication/Authorization Bypass
Vulnerablewindow.addEventListener("message", function(e) {
if (e.data.action === "setUser") {
currentUser = e.data.username; // No origin check!
grantAccess(currentUser);
}
});
Exploitation: Attacker can impersonate any user by sending crafted messages
3. Weak Origin Validation
Substring Matching
// VULNERABLE
if (e.origin.indexOf("trusted.com") !== -1) {
// evil-trusted.com passes!
// trusted.com.evil.com passes!
}
StartsWith Check
// VULNERABLE
if (e.origin.startsWith("https://trusted")) {
// https://trusted-evil.com passes!
}
Regex Bypass
// VULNERABLE
if (/^https:\/\/.*\.trusted\.com$/.test(e.origin)) {
// https://evil.com#.trusted.com passes!
}
4. Prototype Pollution via postMessage
Vulnerablewindow.addEventListener("message", (e) => {
if (e.origin === "https://trusted.com") {
Object.assign(config, e.data); // Prototype pollution!
}
});
Attack payload:
postMessage({
"__proto__": {
"isAdmin": true,
"role": "administrator"
}
}, "https://victim.com");
5. Open Redirect via postMessage
Vulnerablewindow.addEventListener("message", (e) => {
if (e.data.redirect) {
window.location = e.data.redirect; // Open redirect!
}
});
6. CSRF Token Theft
Vulnerable// Parent window sends sensitive data without checking targetOrigin
iframe.contentWindow.postMessage({
csrfToken: document.querySelector('[name=csrf]').value
}, "*"); // Any iframe can receive this!
Real-World CVE Examples
| Product | Vulnerability | Impact |
|---|---|---|
| Missing origin check | Account takeover via OAuth token theft | |
| Slack | Weak origin validation | XSS leading to workspace compromise |
| WordPress | postMessage to eval() | Remote code execution in admin panel |
| Microsoft Teams | Insufficient origin check | Cross-site scripting and data exfiltration |
Security Best Practices
✓ Secure Origin Validation
// RECOMMENDED: Exact match
const TRUSTED_ORIGINS = [
"https://app.trusted.com",
"https://admin.trusted.com"
];
window.addEventListener("message", (event) => {
if (!TRUSTED_ORIGINS.includes(event.origin)) {
console.warn("Rejected message from untrusted origin:", event.origin);
return;
}
// Process message
handleMessage(event.data);
});
✓ Input Validation and Sanitization
function handleMessage(data) {
// Validate structure
if (typeof data !== "object" || !data.action) {
return;
}
// Whitelist allowed actions
const allowedActions = ["updateProfile", "refreshData"];
if (!allowedActions.includes(data.action)) {
return;
}
// Sanitize data before use
const sanitized = DOMPurify.sanitize(data.content);
// Safe to process
processAction(data.action, sanitized);
}
✓ Specify Target Origin When Sending
// GOOD: Specific target origin
iframe.contentWindow.postMessage({
token: sessionToken,
userId: currentUserId
}, "https://widget.trusted.com"); // Exact origin
// BAD: Wildcard target origin
iframe.contentWindow.postMessage({
token: sessionToken // Any origin can receive this!
}, "*");
✓ Use Structured Message Format
// Define message schema
const MessageSchema = {
type: "string", // Message type identifier
version: "number", // Schema version
payload: "object", // Actual data
timestamp: "number", // Message timestamp
nonce: "string" // Replay protection
};
function createMessage(type, payload) {
return {
type: type,
version: 1,
payload: payload,
timestamp: Date.now(),
nonce: crypto.randomUUID()
};
}
function validateMessage(data) {
if (!data || typeof data !== "object") return false;
if (typeof data.type !== "string") return false;
if (typeof data.version !== "number") return false;
if (!data.payload) return false;
// Check message age (prevent replay attacks)
const messageAge = Date.now() - data.timestamp;
if (messageAge > 60000) return false; // 1 minute max
return true;
}
✓ Content Security Policy (CSP)
// Add CSP header to restrict frame-ancestors
Content-Security-Policy: frame-ancestors 'self' https://trusted.com;
// This prevents your page from being framed by untrusted origins
// reducing postMessage attack surface
✓ Avoid Dangerous Operations
Never Use eval()
// NEVER DO THIS
window.addEventListener("message", (e) => {
eval(e.data.code); // RCE!
});
Avoid innerHTML
// DANGEROUS
window.addEventListener("message", (e) => {
div.innerHTML = e.data; // XSS!
});
Use Safe APIs
// SAFE
window.addEventListener("message", (e) => {
div.textContent = e.data; // Safe
// Or use DOMPurify
div.innerHTML = DOMPurify.sanitize(e.data);
});
✓ Defense in Depth
- Origin Validation: Always check event.origin
- Input Validation: Validate message structure and content
- Output Encoding: Sanitize before inserting into DOM
- CSP Headers: Restrict framing capabilities
- Rate Limiting: Prevent message flooding
- Audit Logging: Log postMessage activity for monitoring
Practical Examples
Secure iframe Communication
Parent Page// parent.html
const iframe = document.getElementById("childFrame");
const CHILD_ORIGIN = "https://child.example.com";
// Send message to iframe
function sendToChild(data) {
iframe.contentWindow.postMessage(data, CHILD_ORIGIN);
}
// Receive messages from iframe
window.addEventListener("message", (event) => {
if (event.origin !== CHILD_ORIGIN) {
return;
}
console.log("Received from child:", event.data);
});
Child iframe// child.html
const PARENT_ORIGIN = "https://parent.example.com";
// Send message to parent
function sendToParent(data) {
window.parent.postMessage(data, PARENT_ORIGIN);
}
// Receive messages from parent
window.addEventListener("message", (event) => {
if (event.origin !== PARENT_ORIGIN) {
return;
}
console.log("Received from parent:", event.data);
});
Secure OAuth Flow with Popup
Main Window// Open OAuth popup
const authPopup = window.open(
"https://oauth-provider.com/authorize",
"oauth",
"width=600,height=700"
);
// Listen for OAuth callback
window.addEventListener("message", (event) => {
// Validate origin
if (event.origin !== "https://oauth-provider.com") {
return;
}
// Validate message structure
if (!event.data || !event.data.type || event.data.type !== "oauth_callback") {
return;
}
// Extract and validate token
const token = event.data.accessToken;
if (!token || typeof token !== "string") {
return;
}
// Close popup
if (authPopup && !authPopup.closed) {
authPopup.close();
}
// Use token
authenticateUser(token);
});
Secure Widget Integration
Widget SDKclass SecureWidget {
constructor(iframeElement, allowedOrigin) {
this.iframe = iframeElement;
this.allowedOrigin = allowedOrigin;
this.messageId = 0;
this.pendingRequests = new Map();
window.addEventListener("message", this.handleMessage.bind(this));
}
handleMessage(event) {
// Validate origin
if (event.origin !== this.allowedOrigin) {
console.warn("Rejected message from:", event.origin);
return;
}
// Validate structure
if (!event.data || !event.data.id || !event.data.type) {
return;
}
// Handle response
if (event.data.type === "response") {
const resolver = this.pendingRequests.get(event.data.id);
if (resolver) {
resolver(event.data.payload);
this.pendingRequests.delete(event.data.id);
}
}
}
async sendRequest(method, params) {
return new Promise((resolve, reject) => {
const id = ++this.messageId;
this.pendingRequests.set(id, resolve);
this.iframe.contentWindow.postMessage({
id: id,
type: "request",
method: method,
params: params
}, this.allowedOrigin);
// Timeout after 5 seconds
setTimeout(() => {
if (this.pendingRequests.has(id)) {
this.pendingRequests.delete(id);
reject(new Error("Request timeout"));
}
}, 5000);
});
}
}
// Usage
const widget = new SecureWidget(
document.getElementById("widget-frame"),
"https://widget.trusted.com"
);
widget.sendRequest("getUserData", { userId: 123 })
.then(data => console.log(data))
.catch(err => console.error(err));
Interactive Demo
Try postMessage Communication
This demo shows secure communication between parent and child windows.
Testing for postMessage Vulnerabilities
Manual Testing Approach
- Identify postMessage listeners:
// In browser console window.addEventListener("message", function(e) { console.log("Origin:", e.origin); console.log("Data:", e.data); console.log("Source:", e.source); }, true); - Search for postMessage in JavaScript:
// Use browser DevTools or grep source grep -r "postMessage" ./js/ grep -r "addEventListener.*message" ./js/ - Test origin validation:
// From attacker-controlled page window.opener.postMessage("malicious payload", "*"); // Or for iframes parent.postMessage("malicious payload", "*");
Automated Testing with Python
Python Scannerimport re
import requests
from bs4 import BeautifulSoup
def scan_postmessage_vulnerabilities(url):
"""
Scan a URL for potential postMessage vulnerabilities
"""
try:
response = requests.get(url)
soup = BeautifulSoup(response.content, 'html.parser')
vulnerabilities = []
# Find all script tags
scripts = soup.find_all('script')
for script in scripts:
if script.string:
content = script.string
# Check for postMessage listeners
if 'addEventListener' in content and 'message' in content:
# Check for missing origin validation
if not re.search(r'event\.origin|e\.origin', content):
vulnerabilities.append({
'type': 'Missing origin validation',
'severity': 'High',
'description': 'postMessage listener without origin check'
})
# Check for dangerous operations
if 'eval(' in content:
vulnerabilities.append({
'type': 'eval() with postMessage',
'severity': 'Critical',
'description': 'eval() used with message data'
})
if '.innerHTML' in content:
vulnerabilities.append({
'type': 'innerHTML with postMessage',
'severity': 'High',
'description': 'innerHTML assignment with message data'
})
# Check for insecure message sending
if 'postMessage' in content:
if re.search(r'postMessage\([^,]+,\s*["\']\\*["\']', content):
vulnerabilities.append({
'type': 'Wildcard targetOrigin',
'severity': 'Medium',
'description': 'postMessage using wildcard (*) target'
})
return vulnerabilities
except Exception as e:
print(f"Error scanning {url}: {e}")
return []
# Usage
url = "https://example.com"
vulns = scan_postmessage_vulnerabilities(url)
for vuln in vulns:
print(f"[{vuln['severity']}] {vuln['type']}: {vuln['description']}")
Browser Extension for Testing
Content Script// postmessage-monitor.js
(function() {
const originalAddEventListener = EventTarget.prototype.addEventListener;
const listeners = [];
EventTarget.prototype.addEventListener = function(type, listener, options) {
if (type === 'message') {
listeners.push({
target: this,
listener: listener.toString(),
stack: new Error().stack
});
console.log('[postMessage Monitor] New message listener registered');
console.log('Listener code:', listener.toString());
}
return originalAddEventListener.call(this, type, listener, options);
};
// Intercept postMessage calls
const originalPostMessage = window.postMessage;
window.postMessage = function(message, targetOrigin, transfer) {
console.log('[postMessage Monitor] Message sent:');
console.log('Message:', message);
console.log('Target Origin:', targetOrigin);
if (targetOrigin === '*') {
console.warn('[SECURITY] Wildcard (*) targetOrigin used!');
}
return originalPostMessage.call(this, message, targetOrigin, transfer);
};
// Log all received messages
window.addEventListener('message', function(event) {
console.log('[postMessage Monitor] Message received:');
console.log('Origin:', event.origin);
console.log('Data:', event.data);
console.log('Source:', event.source);
}, true);
})();
Burp Suite Extension
Check for postMessage vulnerabilities in proxy traffic and inject test payloads.
Additional Resources
📚 Specifications
🔒 Security Research
- OWASP - postMessage Security
- PortSwigger Web Security Academy
- HackerOne Reports
🛠️ Tools
- PMHook - postMessage hooking
- Burp Suite - Traffic interception
- DOMPurify - XSS sanitization