Engineering Journal
Schema Editor
Schema Editor

Postmortem: We Shipped postMessage('*') Because It Worked and We Didn't Think About Who Else Would Receive It

2026-06-04

TLDR

postMessage with '*' origin was used throughout the tool's cross-frame communication layer. It worked correctly in the intended VS Code extension context. A security audit identified it as origin validation missing on every outbound message. One-character fix, low exploitation complexity, high potential impact if user data ever passed through the channel.


The Assumption That Seemed Reasonable

The tool runs as a VS Code webview. The VS Code extension host loads it in an iframe. The postMessage calls communicate state between the webview and the extension. VS Code controls the iframe environment. An attacker cannot load a VS Code webview in an arbitrary web page.

This reasoning is approximately correct. VS Code webviews run in a sandboxed context. The direct exploitation path (a web page embedding the webview and capturing its messages) is not straightforward in practice.

The reasoning is wrong about the general principle. Security controls on postMessage origin are not about whether you can imagine an attack; they are about whether the communication channel has a defined audience. '*' means "any audience." That is not a defined audience.

When It Failed

A formal security audit used a grep for postMessage across the codebase. Every call:

window.parent.postMessage({ type: 'cws:ready' }, '*');
window.parent.postMessage({ type: 'tool:state', payload: canvasState }, '*');

The audit flagged every '*' occurrence as origin validation missing. The finding: any embedding context receives these messages. For cws:ready, the impact is low: an attacker learns the tool is ready. For tool:state, the impact depends on what canvasState contains.

At the time of the audit, canvasState did not contain credentials or personal data. But the channel was designed to be extensible. The next feature could add user data to the payload. The '*' origin would pass that data to any embedder without a change to the message-sending code.

The audit recommendation: replace '*' with window.location.origin in every postMessage call. Add origin validation on the receiver side.

What Was Actually Wrong

The '' origin was not a deliberate choice. It was the default from tutorial examples. The first postMessage was written with '' because that is what the example showed. Subsequent postMessage calls copied the pattern. No review step checked the origin argument.

The underlying issue: the semantics of '' were not understood at the time of writing. '' is commonly described as "send to any origin" in tutorials, which sounds like a broadcastability feature. It is more accurately described as "disable origin checking entirely," which sounds like a security decision.

What Got Deleted

The '*' origin argument in every postMessage call: three calls in the main tool code and two in the initialization script.

What Replaced It

All five postMessage calls updated to window.location.origin. A CwsBridge utility extracted as a single module to own all cross-frame communication:

// single module: all postMessage calls go through here
const CwsBridge = {
    send(type, payload) {
        try {
            window.parent.postMessage({ type, payload }, window.location.origin);
        } catch (_) {}
    },
    onData(handler) {
        window.addEventListener('message', (e) => {
            if (e.origin !== window.location.origin) return;
            handler(e.data);
        });
    }
};

Future additions to the cross-frame channel go through CwsBridge.send(). The origin is configured in one place. A grep for postMessage now finds only CwsBridge's implementation.

The Lesson

Copy-pasted security-relevant code carries the security properties of the source. Tutorial examples use '*' because they are illustrating the API, not modeling production security. The origin argument to postMessage is always a security decision, not a convenience parameter.

Read this post in the full Engineering Journal →