Integration
Published
Widgets, embedded checkouts, editor plugins and preview panes all end up talking to their host page through cross-document messages. The conversation looks internal, so it tends to be designed as if it were. It is not: it is an interface anyone in that page can watch and write to, and protecting the code at both ends changes none of that.
A listener is an entry point, not an internal call
Cross-document messaging looks like a function call between two parts of your own application, and that resemblance is the source of most of the trouble. A message handler is closer to an unauthenticated endpoint that happens to be reachable from inside the browser instead of over the network.
The reachability is the part worth internalising. Any document that can obtain a reference to your window can post to it: the page embedding your frame, sibling frames, a window that opened yours, and browser extensions running in the user context. None of them need to have read your code to do it. They need the reference and a guess at the message shape, and the second of those is not much of an obstacle.
Which means the interesting question is not whether someone can read the handler. It is what the handler does when it receives a message it did not expect from a sender it did not verify.
The protocol is discoverable by watching, not by reading
Teams reach for protection here on the assumption that an unreadable bundle makes the message format private. It does not, because the format is delivered to the observer as a matter of normal operation.
Anything running in the embedding page can register a listener of its own and record every message that passes, with its field names, its values and its ordering. Exercise the widget for a few minutes and you have the protocol, in the same way that watching requests in a network panel gives you an HTTP API. This is the same structural point made about WebSocket protocols and GraphQL documents, and it lands harder here, because a message channel does not even require a network capture to observe.
What protection does buy is worth stating precisely so it is not confused with secrecy. It raises the cost of lifting your implementation, which matters if your value is in what the widget does rather than in how it talks. Someone who watches the wire learns your interface. Someone who wants your rendering engine, your pricing logic or your scoring model still has to reimplement it, and that is the cost the transform is actually raising.
The rename trap, and why it fails silently
There is a specific and avoidable way this breaks during a protection rollout, and it produces a failure mode that is unusually annoying to diagnose.
The keys in the object you post are property names, and member renaming rewrites property names. The documentation for that option states the general rule plainly: anything outside your own code that refers to a property by name breaks in the same silent way, including serialised data whose keys match your object shape, and reflection through key enumeration or destructuring sees the new names too. A message payload is exactly that kind of serialised data, and the code reading it is very often a different build.
So a handler that switches on a message type finds no branch matching, and does nothing. No exception is thrown, because nothing went wrong in any sense the runtime can detect. One interactive region stops responding and everything else on the page is fine, which is the hardest shape of bug to trace back to a build setting.
The fix is to treat the message vocabulary as a boundary and reserve it, in the same way you would reserve any name that has to survive independently of your build. That means the message type names, the payload field names and any identifiers the two sides agree on in advance. It is worth writing the list down rather than discovering it, because the member protection documentation makes the point that if you cannot enumerate what refers to your members, the option should start off rather than on.
Origin checking is the control, and it goes on both ends
The message channel has exactly one security primitive, and it is the origin. Everything else is application logic on top of it.
On the receiving side, validate the reported origin of every inbound message against a list you control, before you look at the payload at all. On the sending side, name the exact target origin rather than accepting any destination, so that a frame which has been navigated somewhere unexpected does not receive data intended for your own document. The second half is missed more often than the first, because it looks like a convenience setting rather than a control.
After the origin check, validate the payload the way you would validate a request body: expected fields, expected types, expected ranges. A message that passed the origin check still arrived from another document, and the sending document may be running code you did not write.
None of this is affected by how the code was built, which is the real point. A protected handler with no origin check is an unauthenticated endpoint that is slightly tedious to read. An unprotected handler with a correct origin check is doing the job.
What the domain lock does inside a frame
Because this comes up whenever frames are involved, it is worth being exact about the runtime guard people expect to help here.
The domain lock reads the hostname of the document it is executing in and compares it against a comma-separated allowlist. Inside a frame you created on your own site, that is your own hostname, so the guard matches and permits execution. It has authorised nothing about who embedded you, because it never looked outside its own document.
The other direction is worse for you. The guard reads the hostname first and fires its failure action immediately if the value is empty, before the allowlist is consulted at all. Documents created from inline source, blank pages or object URLs present exactly that empty value, and those are ordinary techniques for building an embedded surface. The result is that the guard breaks your own frame while doing nothing about a hostile embedder. The framing article works through the response headers that do address embedding, which is where that control actually lives.
The short version
A message channel between documents is a public interface: reachable by anything holding a window reference, and fully described to anyone who listens for a few minutes. Protecting the code raises the cost of reimplementing your widget and does not make the protocol private. Reserve the message names before enabling member renaming, or accept a silent breakage in whichever half of the conversation you did not rebuild. Then put the security in the origin check on both ends, where it belongs, and validate payloads as if they arrived from a network, because in every sense that matters they did.
Frequently asked questions
Does obfuscating both sides hide our postMessage protocol?
No, because the protocol is observable without reading either side. Anything running in the embedding page can register its own listener and watch the messages go past, complete with their structure and values, and can post messages of its own into your frame. That view is independent of how your code was built. Protection raises the cost of reimplementing your logic; it does not make a channel private when the channel is delivered to the observer by design.
Will renaming members break our message keys?
It will, unless you exclude them, and the failure is quiet. Property names in the object you post are member access like any other, so a rename pass rewrites them, and the receiving side is often a different build or a different party that still expects the original names. There is no build error and no syntax error. The message simply arrives with fields the recipient does not recognise. Treat message keys the same way you treat any name that crosses a build boundary and reserve them explicitly.
What is the actual control for a postMessage channel?
Origin checking on both ends, applied without exception. The receiver validates the origin of every inbound message before acting on it, and the sender specifies an exact target origin rather than a wildcard so the message is not delivered to whatever document happens to occupy the frame. Those two checks are what decide who may talk to you. Everything else, including how the code was built, is secondary to them.
Is a message from our own iframe automatically trustworthy?
It is not, and this is the assumption that causes trouble. A listener receives messages from any document that can obtain a reference to the window, which includes the embedding page, other frames and anything running as an extension in the user context. A message that claims to come from your frame is a claim, and the only part of it your code can verify is the origin the browser reports. Validate that, then validate the shape and range of the payload as you would for any request arriving over a network.
Does the domain lock protect an embedded widget?
Less than teams expect, in both directions. The guard reads the hostname of the document it is running in, so inside a frame you created on your own site it sees your own host, matches the allowlist and permits execution without having authorised anything about the embedder. In frames with no host at all, such as documents created from srcdoc, blank or blob sources, the hostname is empty and the failure action fires before the allowlist is consulted, which breaks your own frames rather than an attacker's. Neither behaviour is a substitute for an origin check.
What should we do about a message protocol we already shipped?
Add the origin validation first, since it is the change with real security value and it is usually small. Then write down the message names, because that list is what you need to exclude from renaming and it is also what tells you how large your compatibility surface really is. If the list surprises you with its size, that is the finding: a protocol that grew one field at a time is now a public interface you are obliged to keep stable, and narrowing it is worth more than protecting it.
Related reading