How Wildcard postMessage Origins Happen in Chrome Extensions and How to Fix Them
Summary
A critical cross-origin message injection vulnerability was found in offscreen.js, where a wildcard "*" target origin in postMessage and a missing event.source check in the message listener allowed any webpage to inject arbitrary messages into a Chrome extension's offscreen iframe. Two targeted lines of code — adding a source validation guard and replacing "*" with "null" — close the attack surface entirely.
Introduction
The offscreen.js file manages communication between a Chrome extension's background service worker and a sandboxed <iframe> used for offscreen processing. This pattern is common in Manifest V3 extensions that need access to DOM APIs unavailable in service workers. But a flaw in the handleMessage function and the postMessage call on line 20 created an open channel that any malicious website could exploit.
The specific problem: iframe.contentWindow.postMessage(request.data, "*") broadcast messages to any origin, and handleMessage accepted responses from any source — including attacker-controlled frames. For developers building Chrome extensions with offscreen documents, this is a subtle but high-impact mistake that's easy to introduce and hard to spot in code review.
The Vulnerability Explained
The Wildcard Origin Problem
When you call postMessage with "*" as the target origin, you're telling the browser: "deliver this message to the target frame regardless of what origin it's on." For a sandboxed iframe (which has a "null" origin), the correct target is "null", not "*".
Here is the vulnerable line (line 20, before the fix):
// VULNERABLE: Any origin can intercept or spoof this message
iframe.contentWindow.postMessage(request.data, "*");
Using "*" means:
1. The message is delivered without origin restriction.
2. Any frame that happens to be the iframe.contentWindow target receives the data, but more critically...
3. The listener side was also unguarded.
The Missing Source Validation Problem
The handleMessage function, which processes responses from the iframe, had no check on where the message came from:
// VULNERABLE: Accepts messages from ANY origin, ANY source
function handleMessage(event) {
if (pendingResponse) {
pendingResponse(event.data);
pendingResponse = null;
}
}
There is no event.source check. There is no event.origin check. Any window, frame, or tab that can fire a message event at this listener can call pendingResponse(event.data) with attacker-controlled data.
Attack Scenario
Consider this concrete exploitation path:
- A user has the vulnerable extension installed.
- The attacker hosts
https://evil.example.comwhich embeds or references the extension's offscreen page. - The attacker's page fires:
javascript // Attacker's page window.postMessage({ type: "OAUTH_RESPONSE", token: "attacker-token" }, "*"); - Because
handleMessageaccepts messages from any source,pendingResponseis called with the attacker's craftedevent.data. - The extension processes the forged response as if it came from its own trusted iframe — potentially storing a fake OAuth token, triggering privileged actions, or corrupting extension state.
In an extension that handles OAuth tokens or API keys (as described in the broader vulnerability context), this could directly lead to credential theft or account takeover.
The Fix
Two surgical changes were made to offscreen.js:
Change 1: Add event.source Validation (Line 9)
// BEFORE
function handleMessage(event) {
if (pendingResponse) {
pendingResponse(event.data);
pendingResponse = null;
}
}
// AFTER
function handleMessage(event) {
if (event.source !== iframe.contentWindow) return; // ← NEW GUARD
if (pendingResponse) {
pendingResponse(event.data);
pendingResponse = null;
}
}
The new first line if (event.source !== iframe.contentWindow) return; immediately discards any message that didn't originate from the specific iframe element the extension controls. Even if an attacker manages to send a message event to this listener, it will be silently dropped because event.source will be the attacker's window, not iframe.contentWindow.
Change 2: Replace Wildcard with "null" (Line 20)
// BEFORE
iframe.contentWindow.postMessage(request.data, "*");
// AFTER
iframe.contentWindow.postMessage(request.data, "null");
Sandboxed iframes — including Chrome extension offscreen documents — have an origin of "null". By specifying "null" as the target origin, the browser will only deliver the message if the iframe's actual origin matches "null". This prevents the message from being delivered to any other frame that might be substituted or spoofed in place of the expected iframe.
Why Both Changes Are Necessary
These two fixes work together:
| Fix | What it prevents |
|---|---|
event.source !== iframe.contentWindow guard |
Prevents external windows from injecting fake responses into handleMessage |
postMessage(data, "null") instead of "*" |
Prevents the extension's outbound messages from being delivered to wrong-origin frames |
Either fix alone reduces the attack surface, but both together enforce the complete principle of least privilege for this message channel.
Prevention & Best Practices
1. Never Use "*" as a postMessage Target Origin in Extensions
The "*" wildcard should be reserved only for cases where you genuinely don't care which origin receives the message — which is almost never the right answer in a security-sensitive context like an extension.
// ❌ Bad
iframe.contentWindow.postMessage(data, "*");
// ✅ Good (sandboxed iframe)
iframe.contentWindow.postMessage(data, "null");
// ✅ Good (known origin)
iframe.contentWindow.postMessage(data, "https://your-extension-origin.com");
2. Always Validate Both event.source and event.origin
window.addEventListener("message", (event) => {
// Check source identity
if (event.source !== trustedIframe.contentWindow) return;
// Check origin (for non-sandboxed iframes)
if (event.origin !== "https://trusted-origin.example.com") return;
// Safe to process event.data
handleTrustedMessage(event.data);
});
3. Validate and Sanitize event.data
Even with source and origin checks, treat event.data as untrusted input. Validate its structure and type before acting on it:
function handleMessage(event) {
if (event.source !== iframe.contentWindow) return;
// Validate data shape
if (typeof event.data !== "object" || !event.data.type) return;
if (!ALLOWED_MESSAGE_TYPES.includes(event.data.type)) return;
// Now safe to process
processMessage(event.data);
}
4. Use Content Security Policy (CSP)
Chrome extensions should use a strict CSP in their manifest to limit what scripts can run and what origins can communicate with the extension:
{
"content_security_policy": {
"extension_pages": "script-src 'self'; object-src 'self';"
}
}
5. Relevant Standards
- CWE-346: Origin Validation Error
- OWASP: HTML5 Security Cheat Sheet — postMessage
- MDN: Using postMessage safely
Key Takeaways
postMessage(data, "*")inoffscreen.jswas the root cause — using the wildcard target origin in a Chrome extension's offscreen document is never appropriate and creates an open message channel.handleMessagehad no source guard — the absence of anevent.source !== iframe.contentWindowcheck meant any window could trigger thependingResponsecallback with forged data.- Sandboxed iframes have
"null"origin, not"*"— the correct postMessage target for a sandboxed iframe is the string"null", a detail that's easy to get wrong. - Both the sender and receiver must be hardened — fixing only the
postMessagecall or only the listener would leave a partial vulnerability; both changes are required for complete protection. - This pattern is common in Manifest V3 extensions — any extension using offscreen documents for OAuth, crypto, or DOM work should audit its
postMessagecalls immediately.
How Orbis AppSec Detected This
- Source: Chrome extension background service worker passing
request.dataviachrome.runtime.onMessageintoiframe.contentWindow.postMessage - Sink:
iframe.contentWindow.postMessage(request.data, "*")onoffscreen.js:20, and the unguardedpendingResponse(event.data)call inhandleMessage - Missing control: No
event.sourcevalidation in themessageevent listener, and no explicit target origin restriction in thepostMessagecall - CWE: CWE-346 — Origin Validation Error
- Fix: Added
if (event.source !== iframe.contentWindow) return;as the first line ofhandleMessage, and changed the postMessage target from"*"to"null"
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
The wildcard origin vulnerability in offscreen.js is a textbook example of how a single character — the "*" in postMessage — can open a significant attack surface in an otherwise well-structured Chrome extension. The fix is minimal: two lines of code that enforce the principle of least privilege for cross-origin communication. For developers building Manifest V3 extensions with offscreen documents, auditing every postMessage call and every message event listener for proper origin and source validation should be a standard part of the security review checklist.
Cross-origin messaging is a powerful browser feature, but it demands explicit trust boundaries. When those boundaries are left open, attackers will find them.