
How to Test postMessage Listeners for Trust Boundary Failures
Why postMessage Is a Trust Boundary
postMessage is one of those browser features that looks harmless until you trace what the receiver does with the data. The sender might be another tab, a third-party iframe, or a popup you no longer control. Once a listener treats that message as a command, you have crossed a trust boundary.
I usually test these listeners the same way I test an API endpoint: first ask what is trusted, then try to break that assumption without breaking the browser. The browser will deliver the message. The bug lives in the code that accepts it.
The Bugs You Actually Find
Origin Checks That Look Right but Fail
A common pattern is a check that feels strict but is still too broad:
- string matching on
originwithincludes() - comparing against the wrong environment value
- allowing both
httpandhttpsin production - trusting
event.originwithout checkingevent.source
That last one matters more than people expect. A valid origin does not mean the message came from the frame you intended.
Data Validation Gaps After the Origin Check
Even when the origin check is correct, the code often assumes the payload is safe. I see listeners that accept any object with a type field and then branch into dangerous actions:
- navigation
- token refresh
- state changes
- privileged UI actions
- backend requests triggered from the page
The origin check is not the end of the job. It only tells you who spoke. You still need to inspect what they said.
Target Window and Frame Assumptions
Another quiet failure is assuming the sender is the same window you opened or embedded. If the app listens on window, but the action should only come from one specific iframe or popup, the listener needs to compare event.source to that exact reference.
If it does not, any same-origin page, compromised subframe, or unrelated window can become a sender.
A Safe Test Setup in the Browser
Build Two Local Origins
For a clean test, run two local origins on different ports. That is enough to simulate cross-origin behavior without touching real sites.
- Start a receiver app on
http://localhost:3000 - Start a sender page on
http://localhost:3001 - Open both in the browser and use devtools to watch messages
You can also use a minimal local HTML file if you do not want a framework involved. The goal is to control both sides and observe whether the listener accepts unexpected input.
Send Controlled Messages and Watch the Listener
I test three things first:
- a message from the expected origin
- a message from the wrong origin
- a message with the right origin and the wrong shape
That gives you fast coverage of both boundary checks and schema validation. If the listener reacts to all three, the bug is usually obvious.
JavaScript Examples for Reproducing Failures
A Minimal Receiver With a Weak Check
window.addEventListener("message", (event) => {
if (event.origin.includes("localhost:3001")) {
if (event.data.type === "setRole") {
document.body.dataset.role = event.data.role;
}
}
});
This looks defensive, but it is weak in two ways. First, includes() is a fragile origin check. Second, role is accepted without any validation. If that value controls UI state or permissions, the page has handed the sender too much power.
A Test Sender That Proves the Bypass
const target = window.open("http://localhost:3000");
setTimeout(() => {
target.postMessage(
{ type: "setRole", role: "admin" },
"http://localhost:3000"
);
}, 1000);
That sender is safe for testing because it only exercises a local target and sends a harmless string. If the receiver changes privileged state from this message alone, the trust boundary is too wide.
What to Inspect in Real Apps
Listener Registration
Start by finding every message listener. I usually search for:
addEventListener("message"window.onmessage- framework wrappers around message handlers
Then ask which window owns the listener and whether multiple handlers are registered. Duplicate listeners often create inconsistent policy, especially when one path checks origin and another does not.
Message Shape and Schema
Check whether the code validates the entire message or only one field. Good listeners usually define a narrow schema:
- exact
typevalues - required fields
- allowed enum values
- expected data types
- size limits for strings and arrays
Bad listeners branch on event.data.action and trust everything else.
Side Effects Triggered by Message Content
The important question is not “does it parse?” It is “what happens next?” I look for effects such as:
- writing to
localStorage - changing authentication state
- triggering fetch requests
- clicking hidden controls
- redirecting the page
- updating privileged application state
If a message can cause side effects, treat it like an API route.
Fixing the Boundary
Strict Origin and Source Checks
Use exact origin matches, not substring checks. When a specific frame or popup is expected, compare event.source to the known reference too.
const allowedOrigin = "http://localhost:3001";
const expectedWindow = popupRef;
window.addEventListener("message", (event) => {
if (event.origin !== allowedOrigin) return;
if (event.source !== expectedWindow) return;
});
Narrow Message Schemas
Parse only the messages you expect. Reject unknown type values and invalid shapes early. If possible, define a small schema and validate it before any side effect runs.
A plain object check is not enough if nested fields drive behavior. Be strict on every field that changes state.
Safer Frame and Window Handling
If the app opens a popup or embeds a frame, keep a direct reference and only accept messages from that reference. Do not let “any window from the right origin” act like the intended partner.
That one change removes a lot of accidental trust.
Checklist for Manual and Automated Testing
- verify the listener origin check uses exact match
- confirm
event.sourceis checked when a specific sender is expected - send a valid message from the wrong frame and ensure it is rejected
- send an invalid payload from the correct origin and ensure it is rejected
- inspect side effects after accepted messages
- confirm no privileged state changes happen before schema validation
- add regression tests for both origin and message shape
Conclusion
postMessage is safe when the receiver treats it like untrusted input. The bug is usually not the browser API itself. It is the habit of assuming that a familiar origin means a trusted command.
If you test the origin check, the source check, and the message schema separately, you will catch most real failures before they turn into reportable issues.


