Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Vulnerability Research

Node.js vm Module Is Not a Security Mechanism: Escapes & Fixes

"The vm module is not a security mechanism" — why untrusted code escapes via prototype chain attacks, why vm2 failed, and secure isolated-vm alternatives.

Offensive360 Security Research Team — min read
Node.js security vm module sandbox escape JavaScript security SAST code injection vm module security vm module not a security mechanism

If you search the Node.js documentation for the vm module, you’ll find this warning prominently displayed:

“The vm module is not a security mechanism. Do not use it to run untrusted code.”

Despite this explicit warning, developers regularly reach for vm.runInNewContext(), vm.Script, or vm.runInThisContext() as a way to sandbox or isolate untrusted JavaScript. The result is almost always a security vulnerability — often a critical one.

This post explains exactly why the vm module cannot sandbox untrusted code, how attackers escape its “sandbox,” and what you should use instead.


What the vm Module Is Actually For

The Node.js vm module exists to compile and run JavaScript in a V8 Virtual Machine context — meaning it gives you control over the JavaScript execution environment (global variables, built-in objects, etc.). It is designed for use cases like:

  • Template engines that need to evaluate user-defined expressions in a controlled scope (with trusted inputs)
  • Test runners that need to execute code with custom global objects
  • Build tools that need to evaluate configuration files
  • REPLs and playgrounds where the code author is known and trusted

None of these use cases involve running untrusted code from external users. When developers use vm to isolate untrusted input — to build a “sandbox” — they are misusing the module for something it was never designed to do.


Why vm Is Not a Sandbox

The Prototype Chain Is Shared

JavaScript objects in a vm context share the prototype chain with the host Node.js process. This means that code running inside a vm context can reach back into the host environment via prototype manipulation.

const vm = require('vm');

// Attempt to "sandbox" untrusted code
const sandbox = { result: null };
vm.runInNewContext(`
  result = this.constructor.constructor('return process')();
`, sandbox);

console.log(sandbox.result); // Access to the Node.js process object!

In this classic escape, the attacker uses this.constructor.constructor to access the Function constructor from outside the sandbox context, then calls it to return the global process object. From there, they have full access to the Node.js environment — file system, network, child_process, and everything else.

The Function Constructor Escape

const vm = require('vm');
const sandbox = {};
const ctx = vm.createContext(sandbox);

// Attacker-supplied "untrusted" code
const malicious = `
  const f = this.constructor.constructor;
  const proc = f('return process')();
  proc.mainModule.require('child_process').execSync('id');
`;

vm.runInContext(malicious, ctx);
// Executes shell commands on the host system

This escape works because vm.createContext() does not create a fully isolated JavaScript engine. It creates a new context within the same V8 instance, and V8 contexts share the same set of built-in constructors at a lower level than JavaScript can see.

Node.js Built-ins Are Accessible

Even without the prototype escape, many Node.js built-ins can be accessed from inside a vm context if the sandbox object is not carefully constructed:

const vm = require('vm');

// Looks isolated, but...
const result = vm.runInNewContext(`
  const x = {}; 
  x.__proto__.constructor.constructor('return require')()('fs').readFileSync('/etc/passwd', 'utf8');
`);

Timeout Doesn’t Prevent All Attacks

You can set a timeout on vm.runInNewContext(), but this doesn’t prevent:

  • Synchronous prototype chain attacks (they complete instantly)
  • Resource exhaustion via synchronous CPU-intensive computation before the timeout fires
  • Access to host globals that were accidentally passed into the sandbox

Real-World Exploitation Scenarios

Scenario 1: Custom Formula Evaluators

Many business applications let users define custom calculation formulas — pricing rules, reporting aggregations, conditional logic. Developers sometimes use vm to evaluate these:

// VULNERABLE: evaluating user-supplied formula
const vm = require('vm');

app.post('/calculate', (req, res) => {
  const formula = req.body.formula; // User-supplied
  const result = vm.runInNewContext(formula, { data: req.body.data });
  res.json({ result });
});

An attacker submitting a formula containing the prototype escape achieves Remote Code Execution (RCE) on the server.

Scenario 2: Plugin Systems

Applications that allow user-supplied plugins or extensions sometimes use vm to isolate plugin code:

// VULNERABLE: running "untrusted" plugin code
function runPlugin(pluginCode, context) {
  return vm.runInNewContext(pluginCode, context);
}

Scenario 3: Online Code Playgrounds

Online code execution environments built with vm are particularly dangerous because they explicitly expect untrusted input:

// CRITICALLY VULNERABLE: running arbitrary user code
app.post('/run', (req, res) => {
  try {
    const output = vm.runInNewContext(req.body.code, {
      console: { log: (...args) => output.push(args) }
    });
    res.json({ output });
  } catch (e) {
    res.json({ error: e.message });
  }
});

Any user of this playground can escape the sandbox and execute arbitrary commands on the server.


Secure Alternatives for Running Untrusted Code

If your application genuinely needs to execute untrusted JavaScript, here are the approaches that actually provide isolation:

1. vm2 (Deprecated — Use With Caution)

vm2 was a popular third-party sandbox built on top of the vm module with additional escape protections. However, it was deprecated in 2023 after multiple critical sandbox escape vulnerabilities were discovered that could not be reliably patched at the library level.

Do not use vm2 for new projects.

2. Isolated-vm

isolated-vm by laverdet creates truly isolated V8 isolates — separate JavaScript engine instances that do not share a prototype chain with the host:

const ivm = require('isolated-vm');

const isolate = new ivm.Isolate({ memoryLimit: 128 }); // 128MB memory limit
const context = await isolate.createContext();

// Only explicitly exposed values are accessible inside the isolate
const jail = context.global;
await jail.set('global', jail.derefInto());

// Run untrusted code
const result = await context.eval('1 + 1', { timeout: 1000 });
console.log(result); // 2 — no escape possible

isolated-vm is the most robust Node.js option for running untrusted JavaScript because it leverages V8’s built-in isolate mechanism rather than the context mechanism that vm uses.

3. Worker Threads with Message Passing

Node.js worker_threads can run code in a separate thread. When combined with strict message-passing protocols and no shared memory, they provide a reasonable isolation layer:

const { Worker } = require('worker_threads');
const path = require('path');

function runUntrustedCode(code) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(path.join(__dirname, 'sandbox-worker.js'), {
      workerData: { code },
      // Do NOT pass resourceLimits without careful thought
    });
    
    worker.on('message', resolve);
    worker.on('error', reject);
    
    // Kill the worker after timeout
    setTimeout(() => worker.terminate(), 5000);
  });
}

Note: Worker threads are not a complete security boundary. A compromised worker can still access shared memory if SharedArrayBuffer is used, and may be able to reach Node.js native modules.

4. Subprocess Isolation (Child Process + OS Sandboxing)

The most secure approach for truly untrusted code is running it in a separate process with OS-level sandboxing:

const { spawn } = require('child_process');

function runInSandbox(code) {
  return new Promise((resolve, reject) => {
    // On Linux: use seccomp, namespaces, or firejail
    // On any platform: a separate process at minimum limits blast radius
    const child = spawn('node', ['--max-old-space-size=64', '-e', code], {
      timeout: 5000,
      stdio: ['pipe', 'pipe', 'pipe'],
      env: {}, // Empty environment — no credentials, no NODE_PATH
      uid: sandboxUserId, // Dedicated low-privilege user
    });
    
    let output = '';
    child.stdout.on('data', data => output += data);
    child.on('close', code => code === 0 ? resolve(output) : reject(new Error('Execution failed')));
  });
}

For production use cases, pair this with container isolation (Docker with seccomp profiles, gVisor, or Firecracker microVMs).

5. Avoid It Entirely — Use Expression Languages Instead

For formula evaluation, consider purpose-built expression languages rather than full JavaScript:

  • expr-eval — safe math expression evaluator
  • jexl — JavaScript Expression Language, sandboxed by design
  • filtrex — filter expression evaluator

These libraries only evaluate arithmetic and logical expressions — they cannot execute arbitrary code.


How SAST Tools Detect vm Module Misuse

A good Static Application Security Testing (SAST) tool will flag dangerous uses of the vm module — specifically calls to:

  • vm.runInNewContext(userInput, ...) — where the first argument is user-controlled
  • vm.runInContext(userInput, ...) — same concern
  • vm.runInThisContext(userInput) — exposes the current context
  • vm.Script with user-controlled source code

The detection requires taint analysis — the scanner must trace whether user-supplied data (from HTTP request bodies, URL parameters, WebSocket messages, etc.) flows into the vm execution functions.

Offensive360’s SAST engine performs this taint analysis across Node.js applications, tracing data flow through async operations, middleware chains, and cross-module boundaries to flag vm misuse even when the user input arrives several function calls away from the vulnerable vm.run* call.


Summary

ApproachTrue IsolationProduction ReadyNotes
vm module❌ No❌ NoNot a sandbox — trivially escaped
vm2❌ No (deprecated)❌ NoMultiple unpatched escapes
isolated-vm✅ Yes✅ YesBest Node.js option for JS isolation
Worker threads⚠️ Partial⚠️ PartialOS process, not language-level isolation
Child process + OS sandbox✅ Yes✅ YesMost secure; requires OS-level config
Expression libraries✅ Yes✅ YesOnly for formula/expression use cases

The Node.js documentation says it clearly: the vm module is not a security mechanism. If you need to run untrusted code, use a real isolation primitive — an OS process boundary, a dedicated isolate, or a container — not a JavaScript-level construct that shares a V8 instance with your host application.


Offensive360 SAST detects dangerous uses of vm.runInNewContext and similar patterns in Node.js applications as part of its code injection vulnerability detection. Run a scan on your codebase or book a demo to see it in action.

Offensive360 Security Research Team

Application Security Research

Updated March 26, 2026

Find vulnerabilities before attackers do

Run Offensive360 SAST and DAST against your applications and get a full vulnerability report in minutes.