The Node.js vm module lets you run JavaScript in a separate V8 context, but it is explicitly not a security mechanism, and you should never rely on it to sandbox untrusted or malicious code. The Node.js documentation says so directly, and a long history of escapes proves the point. If you need to run code you do not trust, the vm module is the wrong tool, and this guide explains why and what to reach for instead.
What the vm module actually does
The vm module compiles and runs code within a V8 "context," which is a fresh global scope separate from the calling code's globals. That is genuinely useful for legitimate purposes: evaluating configuration expressions, building a template engine, running plugin code you wrote and trust, or isolating global variable pollution.
const vm = require('node:vm');
const context = { count: 0 };
vm.createContext(context);
vm.runInContext('count += 1;', context);
console.log(context.count); // 1
The key word is context, not sandbox. A separate global scope prevents accidental variable collisions. It does not prevent deliberate malice.
Why it is not a security boundary
The reason untrusted code escapes vm is structural: the guest code and the host share the same V8 isolate and the same process. Objects, prototypes, and functions can leak across the boundary, and once guest code gets a reference to any host object, it can often climb the prototype chain back to powerful host capabilities.
The classic escape uses the constructor of a passed-in object to reach the host's Function constructor and, from there, process and require:
const vm = require('node:vm');
// Illustrative: this pattern has historically let guest code
// reach host globals via the constructor chain.
const escapeAttempt = `
const hostConstructor = this.constructor.constructor;
hostConstructor('return process')();
`;
// Passing host objects/functions into the context widens this surface.
The details of any specific escape change as V8 and Node evolve, and that is exactly the problem: the security posture of vm depends on the absence of a known escape at a given moment, not on a designed boundary. New escapes get found. There is no supported guarantee that guest code cannot reach process, the filesystem, the network, or require.
This is not a bug to be patched. The Node.js maintainers are clear that the vm module was never intended to run untrusted code securely, so a working escape is not treated as a vulnerability in the way a sandbox breach would be.
The vm2 cautionary tale
For years, the community answer was vm2, a popular package that layered proxies and membranes over vm to try to build a real sandbox. It was widely used precisely because people needed what vm does not provide.
The important lesson is how it ended. vm2 accumulated a series of critical sandbox-escape vulnerabilities, and its maintainer ultimately discontinued the project, stating that the security model could not be made reliable and advising users to migrate away. That outcome is the strongest possible signal: even a dedicated, well-maintained library built specifically to sandbox JavaScript in-process could not hold the line. If vm2 could not do it safely, a hand-rolled vm wrapper certainly cannot.
What to use instead
If you must run untrusted code, move the boundary to something the operating system or hardware actually enforces.
Separate processes with strict limits. Run guest code in a child process with no inherited secrets, dropped privileges, resource limits (CPU, memory, time), and no network unless required. A process boundary is a far stronger line than an in-isolate context.
isolated-vm. This library runs code in genuinely separate V8 isolates with separate heaps, giving a much stronger boundary than the built-in vm module because host and guest do not share an isolate. It is the closest thing to in-process isolation that is taken seriously, though you still design carefully around what you expose across the bridge.
Containers and microVMs. For higher assurance, run untrusted workloads in containers with seccomp and dropped capabilities, or in lightweight microVMs such as Firecracker or gVisor-backed sandboxes. These are what serverless platforms use to run arbitrary customer code, and for good reason.
WebAssembly. Compiling untrusted logic to Wasm and running it in a Wasm runtime gives a memory-safe, capability-scoped execution model where the guest only reaches what you explicitly import.
The right choice depends on your threat model, but the common thread is that the boundary is enforced by something outside the shared V8 isolate.
Practical guidance
If you are reaching for vm today, ask what the code you are running actually is. If you authored it and it is bundled as trusted plugin logic, vm for scope isolation is reasonable. If any of it originates from users, customers, or third parties you cannot fully trust, treat it as hostile and use a real isolation layer.
And know your dependencies. If vm2 is anywhere in your tree, it is unmaintained and carries known escapes, so it should be a priority to remove. Software composition analysis will surface abandoned and vulnerable packages like this across transitive depth, and an SCA tool such as Safeguard can flag an unmaintained library before it becomes an incident. Our DevSecOps academy covers building these checks into CI.
FAQ
Is the Node.js vm module safe for running untrusted code?
No. The official documentation states the vm module is not a security mechanism and must not be used to run untrusted code. Guest and host share the same V8 isolate, so escapes are possible.
Why did vm2 get discontinued?
It accumulated repeated critical sandbox-escape vulnerabilities, and the maintainer concluded the security model could not be made reliable, discontinuing the project and advising migration to stronger isolation.
What is the difference between vm and isolated-vm?
The built-in vm module runs code in a separate context within the same V8 isolate, sharing a heap with the host. isolated-vm runs code in genuinely separate isolates with separate heaps, a much stronger boundary.
What should I use to run untrusted JavaScript?
Move the boundary outside the shared isolate: separate processes with dropped privileges and resource limits, isolated-vm, containers or microVMs like Firecracker, or a WebAssembly runtime, chosen to match your threat model.