Security Risks of Node.js VM Module for Untrusted Code

The node:vm module in Node.js allows developers to compile and run JavaScript code within a V8 virtual machine context. However, contrary to a common misconception, it is not a secure sandbox for executing untrusted user scripts. This article examines the critical security implications of using the node:vm module, demonstrates how easily code can escape its environment, and explores safer alternatives for running untrusted code.

The Core Limitation: Not a Security Sandbox

The most critical security implication of the node:vm module is that it is not designed to be a security barrier. The official Node.js documentation explicitly warns against using it to run untrusted code.

While node:vm can isolate global variables and prevent scripts from directly accessing the parent context’s global scope, it does not isolate the execution environment at the process level. The guest script and the host application still share the same Node.js process and memory space.

How Untrusted Code Escapes the VM

Because the execution context shares the same process, a guest script can easily obtain a reference to the host’s global objects through JavaScript’s prototype chain. Once a script gains access to a host object, it can traverse up to the host’s Function constructor and execute arbitrary system commands.

Here is a classic example of how an attacker can escape a node:vm context:

const vm = require('node:vm');

const untrustedCode = `
  const foreignConstructor = this.constructor.constructor;
  const process = foreignConstructor('return process')();
  process.mainModule.require('child_process').execSync('whoami');
`;

vm.runInNewContext(untrustedCode);

In this exploit: 1. this.constructor retrieves the constructor of the VM’s global context object. 2. .constructor on that retrieves the host’s Function constructor. 3. Calling this constructor allows the creation of a function that executes in the host’s global scope, returning the host’s process object. 4. With access to process, the attacker can load child_process and execute arbitrary shell commands on the host machine.

Denial of Service (DoS) Vulnerabilities

Even if you successfully block prototype chain escapes, the node:vm module remains vulnerable to Resource Exhaustion attacks.

1. CPU Starvation (Infinite Loops)

A simple infinite loop like while(true) {} in the guest script will block the entire Node.js event loop. While the node:vm module provides a timeout option, this timeout only halts synchronous execution. It does not prevent asynchronous tasks or microtasks (like Promise loops) from freezing the process.

2. Memory Exhaustion

The VM shares the host’s heap memory. An attacker can write a script that continuously allocates memory (e.g., appending to a massive array) until the Node.js process runs out of memory (OOM) and crashes, taking down the entire host application.

Secure Alternatives for Untrusted Code

If your application must execute untrusted user-submitted JavaScript, you should avoid node:vm entirely. Instead, use one of the following secure architectures:

1. Use Specialized Sandbox Libraries

Libraries like isolated-vm compile and run code in distinct V8 isolates. Each isolate has its own heap and cannot access the main Node.js process’s memory or APIs, preventing standard escape vectors.

2. Process-Level Isolation (Containers)

Run the untrusted code inside a disposable, unprivileged Docker container or a microVM (like AWS Firecracker). Set strict limits on CPU, memory, and network access at the container level. Once the script finishes execution, destroy the container.

3. Serverless Functions

Offload the execution of untrusted scripts to serverless platforms (like AWS Lambda or Google Cloud Functions). These environments naturally isolate execution per request and automatically handle cleanup and resource limits.