Library / SDK
patriksimek/vm2 avatar
patriksimek/vm2

vm2: an in-process JavaScript sandbox with an honest security disclaimer

Advanced vm/sandbox for Node.js

4,105 stars329 forksJavaScriptMIT

At a glance

What is it?
vm2 runs untrusted JavaScript inside the same Node.js process as your application using Proxies and the internal VM module. Its own README warns that in-process sandboxing is a cat-and-mouse game and points to stronger isolation options.
Who is it for?
Adopt vm2 when the code you run comes from a vetted source (internal plugins, tooling scripts) and you need synchronous access to host objects in a single process. Do not adopt it as the only boundary for arbitrary user submissions; the README itself recommends stronger isolation for that case.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 19 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What vm2 solves, and for whom

Node's built-in vm module creates a new context, but it is not a security boundary. The README demonstrates this with a two-line example: runInNewContext('this.constructor.constructor("return process")().exit()') reaches the host process through the constructor chain. vm2 exists to close that specific gap. It wraps the context with Proxies so that property access, function calls and prototype traversal are mediated before they reach host objects. The audience is narrow and specific: developers who need to execute JavaScript they did not write, inside their own process, with synchronous access to host objects and callbacks. Plugin systems, template or rule engines, and internal tooling that runs user-supplied scripts are the typical fits. If you only need to evaluate a trusted expression, node:vm is enough and vm2 adds overhead for no benefit.

The Proxy bridge and the require override

The README describes three mechanisms: the internal VM module creates the context, Proxies prevent escape, and the built-in require is overridden to control module access. The VM class gives you a context with no Node globals at all, which is why vm.run('process.exit()') throws a TypeError in the README's first example. NodeVM is the higher-level class: it accepts a require option with external and root keys, so the sandbox can pull in modules from a directory you name. The README's example passes external: true and root: './', then requires the request package from inside the sandbox. The Proxy layer is where the complexity lives. JavaScript lets you reach objects through prototype chains, constructor properties on error objects, symbol protocol hooks, and async timing windows. Each of those is a potential path from a sandboxed value back to a host value, and vm2 has to intercept them all. That is why the project maintains docs/ATTACKS.md and a test/ghsa/ directory of regression tests: every published escape becomes a permanent test case.

Installing vm2 and running a first script

The README gives one install command and no build step; the package ships lib, index.js and index.d.ts, and its only runtime dependencies are acorn and acorn-walk. Install it into your project:

bash
npm install vm2

The package declares engines.node >= 6.0, so any current LTS release works. The fastest way to see the sandbox boundary is the VM class, which exposes no Node globals. The README's example expects a TypeError rather than a process exit:

js
import { VM } from 'vm2';

const vm = new VM();
vm.run(`process.exit()`); // TypeError: process.exit is not a function

When the sandboxed code needs modules, switch to NodeVM and declare what it may require. The README's example enables external modules and sets the resolution root, then requires request from inside the script and passes a filename as the second argument to run:

js
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);
    });
`,
	'vm.js',
);

The README also lists a bin entry (./bin/vm2) in package.json, so the package installs a vm2 executable, though the README does not document its flags. Console output from inside the sandbox is controllable, which the feature list calls full control over the sandbox's console output.

The disclaimer is the most important part of the README

Most sandbox projects bury the caveats. vm2 leads with them. The README states plainly that researchers and security professionals continuously discover new ways to escape the sandbox, that new bypasses will likely be discovered in the future, and that vm2 should not be the only line of defense. It also tells readers to check the security advisories page for known vulnerabilities and to keep the package updated. This is not marketing hedging; it is a description of in-process sandboxing as a category. Because the sandbox shares a process, a V8 heap and an event loop with your application, a single missed Proxy trap is a full compromise of the host. There is no second wall behind it. The practical consequence is that vm2 is a mitigation layer, not an isolation layer, and the README says so in those terms. If your threat model assumes an adversary who reads the changelog and tries the newest bypass, vm2 alone does not meet it.

Bun support: compatibility without a security boundary

The runtime table marks Node.js as supported and Bun as experimental, and the README separates two claims that are easy to conflate. First, Bun is not a security boundary: the threat model, docs/ATTACKS.md and every regression test in test/ghsa/ derive from V8 internals, while Bun uses JavaScriptCore, which has its own equivalents that have not been audited against vm2's bridge. A passing suite under Bun shows compatibility, not that the sandbox holds. Second, compatibility is partial. test/bun-skips.js lists what is excluded, and the README names concrete gaps: Buffer.from(arrayLike) returns a zero-length buffer; VMScript filename, lineOffset and columnOffset metadata is not observable because JSC CallSite objects carry no methods; Object.freeze on a frozen host object with a non-configurable accessor throws a proxy-invariant TypeError where V8 does not; and some Buffer operations across the boundary are drastically slower, with a 64 MB allocUnsafe taking over 400 seconds against 1.7 on Node. That last number is from the README, and it is slow enough to read as a hang. The README's own instruction is not to use vm2 on Bun to isolate untrusted code.

When a separate process or isolate is the better answer

The README's alternatives table is unusually direct. isolated-vm uses separate V8 isolates with a different V8 heap, which is fast but is described as in maintenance mode and requiring manual V8 updates. A separate process or Worker thread gives real process isolation at the cost of IPC overhead and serialization, since data crossing the boundary cannot be passed by reference. Containers and microVMs (Docker, gVisor, Firecracker) trade startup overhead and resource weight for hardware or kernel-level separation. Managed execution services move the problem off your infrastructure and add network latency and an external dependency. The difference in approach matters more than the ranking: vm2 mediates access within one heap, while the others put a real boundary between the untrusted code and your process. If you need to pass a callback into the sandbox and get a synchronous return value, only the in-process approach does that cheaply. If you need the code to be unable to touch your process at all, none of vm2's design goals match that requirement.

Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-09-13. Recent releases include v3.12.0 on 2026-09-01, v3.12.1 on 2026-09-03 and v3.12.2 on 2026-09-08, so the release cadence in the weeks before that push was steady. The feature list describes the project as actively maintained with patches for known escape methods, which is consistent with the security advisories the README points to. The licence is MIT, declared in package.json and shipped as LICENSE.md; MIT is permissive and imposes no copyleft obligation on your own code, but it also means no warranty, and the README's disclaimer puts the security burden on the integrator. Read that as an engineering fact, not legal advice; if your organisation has a policy on sandbox dependencies, the security advisories page is the place to check for open issues. Upgrade cost is the real ongoing expense. Because escapes are patched as they are reported, staying on an old version means carrying known bypasses, and the README explicitly asks you to subscribe to advisories and update promptly. There is no documented rollback procedure in the README, so plan version pinning and a test pass before you bump.

Editorial conclusion

Adopt vm2 when the code you run comes from a vetted source (internal plugins, tooling scripts) and you need synchronous access to host objects in a single process. Do not adopt it as the only boundary for arbitrary user submissions; the README itself recommends stronger isolation for that case. Before committing, read docs/ATTACKS.md and the security advisories, check whether your Node version is supported, and decide if the Bun path matters to you, since the README states Bun compatibility is partial and not a security boundary.

Frequently asked questions

How do I install vm2?

Run npm install vm2. The package has no build step for consumers; it ships lib, index.js and index.d.ts, and declares engines.node >= 6.0.

What is the difference between Node's vm module and vm2?

The README shows that node:vm's runInNewContext lets code reach the host process through this.constructor.constructor, while vm2's VM class throws a ReferenceError for the same expression because process is not defined.

Can I run vm2 on Bun?

The README marks Bun as experimental and states it is not a security boundary, since the threat model and regression tests derive from V8 internals while Bun uses JavaScriptCore. Compatibility is also partial, with gaps listed in test/bun-skips.js.

Is vm2 safe for running arbitrary user-submitted code?

The README says no. It states that new bypasses will likely be discovered, that vm2 should not be your only line of defense, and that for completely untrusted sources you should use a solution with stronger isolation guarantees.

How does vm2 let sandboxed code require modules?

NodeVM accepts a require option with external and root keys. The README's example sets external: true and root: './', then calls require('request') from inside the sandbox script.

Official sources

  1. Issues
  2. License: MIT
  3. patriksimek/vm2 on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/patriksimek-vm2.svg)](https://hysenlabs.com/projects/patriksimek-vm2)