
Isolated JavaScript sandbox for Node.js that runs untrusted code with restricted access to built-in modules and host resources via Proxy-based interception.
vm2 is a sandbox that can run untrusted code with whitelisted Node's built-in modules.
npm install vm2
import { VM } from 'vm2';
const vm = new VM();
vm.run(`process.exit()`); // TypeError: process.exit is not a function
import { NodeVM } from 'vm2';
const vm = new NodeVM({
require: {
external: true,
root: './',
},
});
vm.run(
`
var request = require('request');
request('http://www.google.com', function (error, response, body) {
console.error(error);
if (!error && response.statusCode == 200) {
console.log(body); // Show the HTML for the Google homepage.
}
});
`,
'vm.js',
);
Before using vm2, you should understand how it works and its limitations.
vm2 attempts to sandbox untrusted JavaScript code within the same Node.js process as your application. It does this through a complex network of Proxies that intercept and mediate every interaction between the sandbox and the host environment.
JavaScript is an extraordinarily dynamic language. Objects can be accessed through prototype chains, constructors can be reached via error objects, symbols provide protocol hooks, and async execution creates timing windows. The sheer number of ways to traverse from one object to another in JavaScript makes building an airtight in-process sandbox extremely difficult.
We are honest about this reality: Despite our best efforts, researchers and security professionals continuously discover new ways to escape the vm2 sandbox. We actively patch these vulnerabilities as they are reported, but the cat-and-mouse nature of in-process sandboxing means that:
If you require stronger isolation guarantees, consider these alternatives that provide true process or hardware-level isolation:
| Solution | Approach | Performance | Trade-offs |
|---|---|---|---|
| isolated-vm | Separate V8 isolates (different V8 heap) | Fast | In maintenance mode; requires manual V8 updates |
| Separate process / Worker | child_process or Worker threads with limited permissions | Medium | Higher IPC overhead; data must be serialized |
| Containers / VMs | Docker, gVisor, Firecracker | Slow | Startup overhead; resource-heavy |
| Managed services | Cloud-based code execution (e.g., AWS Lambda, Cloudflare Workers) | Variable | Network latency; external dependency |
vm2 can be suitable when:
If you're running code from completely untrusted sources (e.g., arbitrary user submissions), we strongly recommend using a solution with stronger isolation guarantees.
| Runtime | Status |
|---|---|
| Node.js | Supported. The sandbox is a security boundary. |
| Bun | Experimental. Partial functional compatibility — not a security boundary. |
Two separate limitations apply to Bun, and neither implies the other.
It is not a security boundary. vm2's threat model, the attack catalogue in
docs/ATTACKS.md, and every regression test in test/ghsa/
are derived from V8 internals. JavaScriptCore, which Bun uses, has its own
equivalents, and none have been audited against vm2's bridge. The suite passing
under Bun demonstrates compatibility, not that the sandbox holds there. Do not
use vm2 on Bun to isolate untrusted code.
Compatibility is partial, not parity. A green Bun run covers only the tests
that actually execute there. test/bun-skips.js lists what is excluded and why,
and the known behavioural gaps include:
Buffer.from(arrayLike) returns a zero-length bufferVMScript filename / lineOffset / columnOffset metadata is not
observable, because JSC's CallSite objects carry no methodsObject.freeze on a frozen host object with a non-configurable accessor
throws a proxy-invariant TypeError where V8 does notBuffer operations across the sandbox boundary are drastically slower —
a 64 MB allocUnsafe takes over 400 seconds against 1.7 on Node, slow enough
to read as a hangTreat Bun support as best-effort compatibility for trusted code, and check the skip list before relying on any particular behaviour.
For an in-depth look at vm2’s internals, see docs/ATTACKS.md.
Try it yourself:
import { runInNewContext } from "node:vm";
runInNewContext('this.constructor.constructor("return process")().exit()');
console.log('Never gets executed.');
import { VM } from 'vm2';
new VM().run('this.constructor.constructor("return process")().exit()');
// Throws ReferenceError: process is not defined
VM is a simple sandbox to synchronously run untrusted code without the require feature. Only JavaScript built-in objects and Node's Buffer are available. Scheduling functions (setInterval, setTimeout and setImmediate) are not available by default.
Options: