How to Test postMessage Listeners for Trust Boundary Failures

How to Test postMessage Listeners for Trust Boundary Failures

pr0h0
securityweb-developmentcross-origintesting
AI Usage (88%)

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 origin with includes()
  • comparing against the wrong environment value
  • allowing both http and https in production
  • trusting event.origin without checking event.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.

  1. Start a receiver app on http://localhost:3000
  2. Start a sender page on http://localhost:3001
  3. 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 type values
  • 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.source is 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.

Share this post

More posts

Comments