Safeguard
Application Security

postMessage Has No Default Access Control

Your embeddable widget talks to the page hosting it across origins. Anything that can get a reference to that window can send it a message, and unless the listener checks who sent it, the widget will act on a message from anywhere.

Vikram Iyer
Security Researcher
5 min read

Your product ships an embeddable widget: a chat bubble, a checkout form, a support panel, something a customer drops into their own site with a script tag and an iframe. To talk to the page hosting it, that widget uses postMessage, which is the only sanctioned way for two different origins to communicate in a browser.

postMessage has no default access control. Anything that can get a reference to the window, which any page embedding you can, can send it a message, and unless your listener checks who sent it, it will act on a message from anywhere.

This post is what the listener needs to do. For whoever built the embed script.

The two checks almost everyone skips

Verify the origin of incoming messages. A message handler that does not check event.origin will process a message from any page on the internet that knows your widget's message format, which for a widely used embed is publicly knowable.

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://trusted-parent.example.com') return;
  // handle event.data
});

For a widget embedded on many different customer domains, you cannot hardcode one origin. Check against a registered allowlist of domains that customer actually configured for their embed, resolved server-side rather than trusted from the message itself, so a page cannot simply claim to be an authorised parent.

Specify the target origin on outgoing messages. The second half, and the one people miss because it fails silently rather than obviously. Using '*' as the target origin sends your message to whatever is currently in that window, including a page that has navigated away to somewhere else since you last checked, or an iframe an attacker has positioned to receive it.

// leaks the message to anyone currently occupying that window
parent.postMessage(sensitiveData, '*');

// only delivers to the origin you intend
parent.postMessage(sensitiveData, 'https://trusted-parent.example.com');

'*' is the default in a huge number of copy-pasted examples, which is why it is the default in a huge number of shipped widgets.

What a missing origin check actually enables

Data exfiltration. If your widget posts anything sensitive, a session token, form data, user details, to its parent without checking who is listening, any page that can frame your widget receives it. This is the direction most examples focus on and it is real.

Command injection into the widget. The other direction matters just as much and gets less attention: if your widget accepts commands from its parent without checking the sender, any page that can load an iframe pointing at your widget can send it instructions, whatever those instructions can trigger. A widget that accepts a "navigate to this URL" or "submit this form" message with no origin check is remotely controllable by any page that embeds it.

Clickjacking combined with messaging. An attacker frames your widget invisibly, positions a decoy element over the parts a user will click, and uses postMessage from their page to feed your widget staged input while the user believes they are interacting with something else entirely.

Validate the data, not just the sender

A correct origin check is necessary and not sufficient. The message content is still attacker-influenced if the sending origin's own page can be manipulated, for instance through content the origin renders from user input elsewhere.

Treat event.data the same as any other external input: validate its shape, do not eval it or pass it into a templating function, and never assume the structure matches what your own code sends just because the origin matched.

Where the widget itself is embedded matters too

The origin check protects the message channel. It does not protect the widget from being framed by a page you did not intend to host it, which is a related and separate problem.

If your widget is only meant to run on registered customer domains, enforce that with a frame-ancestors content security policy on the widget's own response, so browsers refuse to render it inside an iframe on an unauthorised page at all. This is the correct place for that control, rather than trying to detect framing from inside the widget's own script, which a sufficiently determined page can work around.

Check yours

// In your widget's script, search for every postMessage call and listener
grep -rn "postMessage\|addEventListener('message'" src/

// For each listener: does it check event.origin before acting?
// For each send: does it pass a specific origin, or '*'?

Then test directly: create a page on a different origin, embed your widget or open it as it would normally be embedded, and from your test page's console call postMessage at it with a payload matching what your widget expects. If it responds as though the message were legitimate, the check is missing or ineffective.

The concession

Maintaining a strict per-customer origin allowlist is real ongoing work: every new customer domain needs registering, and a misconfigured or forgotten entry breaks a legitimate integration rather than merely under-protecting it, which is the kind of failure that generates support tickets rather than security reports.

The version worth keeping regardless of how strict the allowlist gets: never use '*' for outgoing messages carrying anything sensitive, and never act on an incoming message without checking event.origin against something server-configured rather than trusting a value in the message itself. Those two rules cost nothing to enforce and they close the most damaging failures even if the allowlist approach is loosened for convenience elsewhere.

The implication

postMessage is a channel with no built-in access control, and every property of the messages it carries, who can send them, who receives them, what they can trigger, is entirely up to code you wrote.

Grep your embed script for the two patterns above. A missing origin check or a '*' target is not a subtle bug; it is a channel left open to whichever page finds it, and for a widget embedded across many sites, someone eventually will.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.