Postmessage And Checking Who Youre Actually Talking To
postMessage is how two different origins are allowed to talk at all. Skip the origin check on either end, and that permission becomes the vulnerability.
The last post ended on a mechanism that's just as exploitable as framing itself when nobody checks who's actually on the other end. postMessage is that mechanism, and the exploit here isn't a bug in the API. It's what happens when you use it without asking who sent the message, or where it's actually going.
Why postMessage Exists at All
The same-origin policy that runs through this entire course exists to stop one origin from freely reaching into another. That's exactly the rule that would make a huge number of normal web patterns impossible if there were no exception at all.
A payment provider's checkout form embedded in someone else's site, a chat widget loaded through an iframe, a popup window that needs to hand data back to whatever opened it, all of these are two different origins that legitimately need to exchange information. postMessage is the sanctioned way to do that.
targetWindow.postMessage(data, targetOrigin);It's a real, necessary tool. The vulnerability shows up on both ends of the conversation, and each end needs its own fix.
The same connection carries messages both ways for the lifetime of the window, not just once. A popup window opened for something like a third-party login flow keeps that link alive until the tab closes, which means every message either side sends during that whole session needs to pass the same checks, not just the first one.
The Receiving End: Trusting Whatever Arrives
Picture a fictional support tool called Denver Chat, embedded on a company's site as an iframe. It listens for incoming messages and renders whatever text arrives directly into the page.
window.addEventListener("message", (event) => {
chatContainer.innerHTML = event.data.html;
});Nothing in that listener checks who sent the message. Literally anything running in the browser can fire a message at that window and have it accepted as legitimate, another tab the user has open, a different iframe on the same page, even a browser extension quietly injecting content.
The fix is a single check, run before anything from the message gets trusted.
const ALLOWED_ORIGINS = ["https://denver-chat.example.com"];
window.addEventListener("message", (event) => {
if (!ALLOWED_ORIGINS.includes(event.origin)) {
return;
}
chatContainer.innerHTML = event.data.html;
});event.origin tells you exactly where the message actually came from, not where the sender claims to be from. Check it against an explicit allow list, and reject silently if it doesn't match. No error, no feedback to the sender, just nothing happens.
Denver Chat's listener, unfixed, would happily render HTML sent by a completely unrelated tab the user happens to have open, or by a browser extension injecting content into every page it runs on. None of those senders are malicious by design. The listener just never asked whether it should trust them, and without that question ever getting asked, the answer defaults to yes for absolutely everything.
The Sending End: Trusting Whatever Is There
The mirror problem lives on the other side, and it's easy to miss because it doesn't look like a mistake at first.
partnerFrame.contentWindow.postMessage(sensitiveData, "*");The wildcard target origin, "*", means: send this data to whatever is currently occupying that window or frame, no matter what it actually is. That's fine right up until the assumption breaks.
If the iframe you expected to be a trusted partner's widget ever gets swapped, redirected, or was never quite what you assumed it was, your data goes straight to it. Nobody has to break anything for this to go wrong, the trust was misplaced from the start.
This is conceptually close to the mirror image of CSRF. CSRF exploits a browser that trusts an incoming request without checking who sent it. This exploits code that trusts an outgoing message's destination without checking what's actually sitting there.
The fix is specifying the real expected origin instead of the wildcard.
partnerFrame.contentWindow.postMessage(
sensitiveData,
"https://partner-widget.example.com"
);The browser itself refuses to deliver the message if the actual destination doesn't match the origin you specified. Not your code, not a manual check, the platform enforces it once you tell it what to expect.
ExpandA receiving page checking event.origin against an allow list before trusting a message, next to a sending page specifying a real target origin instead of a wildcard.
Both Directions, One Habit
Both ends need an origin check, and skipping either one leaves half the problem wide open.
The receiving check protects what you're willing to accept from the outside world. The sending check protects what you're willing to hand to it. Neither one substitutes for the other, and a page that gets one right while ignoring the second is still exposed on whichever side it forgot.
The good news is the fix on each side is genuinely small, a couple of lines each, once you know to look for the missing check. It's the same instinct that shows up everywhere else in this course: verify who you're actually dealing with before you act on what they sent you, or before you hand them something they shouldn't get.
The Essentials
postMessageis a legitimate, necessary way for two different origins to communicate, used by chat widgets, embedded checkouts, and popup windows.- A receiving listener that doesn't check
event.originaccepts messages from anything, including other tabs, other iframes, or a browser extension. - The receiving fix is an explicit allow list checked against
event.origin, rejecting silently on a mismatch. - A wildcard
"*"target origin sends data to whatever currently occupies that window, whether or not it's still the origin you expected. - This is close to a mirror image of CSRF: one trusts an incoming request without checking the sender, the other trusts an outgoing destination without checking what's there.
- The sending fix is specifying the real target origin, letting the browser itself refuse delivery on a mismatch.
Next up: a subtler variant of the same "trick a window" family, one that waits until you've already stopped paying attention.
Further Reading and Watching
- Cross domain and cross window communication in JavaScript | document.domain | Window.postMessage(): a walkthrough of how cross-origin windows are allowed to talk, and where
postMessagefits. - Window: postMessage() method: MDN's full reference, including the exact origin-checking behavior on both the send and receive side.
Keep reading