window.postMessage()

The Complete Guide to Cross-Origin Communication and Security

⚠️ Security Critical API

postMessage is one of the most commonly misused browser APIs. Improper implementation leads to XSS, data theft, and authentication bypass vulnerabilities in major web applications.

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

  1. When Receiving: Always validate event.origin before processing event.data
  2. 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
Facebook 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

  1. Origin Validation: Always check event.origin
  2. Input Validation: Validate message structure and content
  3. Output Encoding: Sanitize before inserting into DOM
  4. CSP Headers: Restrict framing capabilities
  5. Rate Limiting: Prevent message flooding
  6. 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.

Messages will appear here...

Testing for postMessage Vulnerabilities

Manual Testing Approach

  1. 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);
  2. Search for postMessage in JavaScript:
    // Use browser DevTools or grep source
    grep -r "postMessage" ./js/
    grep -r "addEventListener.*message" ./js/
  3. 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

🔒 Security Research

  • OWASP - postMessage Security
  • PortSwigger Web Security Academy
  • HackerOne Reports

🛠️ Tools

  • PMHook - postMessage hooking
  • Burp Suite - Traffic interception
  • DOMPurify - XSS sanitization