Heaps of Built-ins: How JavaScript Sandboxes Work
vm2 accumulated more than twenty documented sandbox escapes before it was deprecated. For years, it was the default answer to a question that has only become more common: how do you run JavaScript you didn't write and don't trust?
That question, once the domain of a few specialists, now belongs to everyone. AI agent frameworks execute model-generated code as a matter of routine. Automation platforms run user-authored scripts on shared infrastructure. Low-code tools let people paste JavaScript into a form field and hit save. In every case, something has to decide what that code is allowed to touch.
The answer is not a longer blocklist. It starts with understanding what a JavaScript engine shares by default between the code you wrote and the code you didn't.
Everything in JavaScript is reachable from everything else
In JavaScript, two important things are shared between all scripts.
The first is the heap. That's the region of memory where the engine allocates anything that outlives a single function call, which, in JavaScript, is nearly everything: objects, arrays, strings, closures, and functions. You never allocate it yourself, and you never free it, because the garbage collector reclaims whatever is no longer reachable.
That word, reachable, is the important one. A heap is what makes an object graph traversable. If two pieces of code allocate into the same heap, either one can hold a reference to something the other created, and from that reference walk to everything connected to it.
The second is the built-ins. These are the objects the engine hands you before your code runs at all: Object, Array, Function, JSON, Promise, RegExp, and all their prototypes. They're global, shared, and mutable in JavaScript.
Which means untrusted code doesn't need to find your objects. It can redefine a built-in’s prototype, and when the victim uses that built-in to perform a task, then attacker-controlled code can execute.
For example, as an attacker, I can define an alternate prototype for the map method of the array built-in object:
// running inside a "sandbox" that shares built-ins with its host
array.prototype.map = function () {
// <MY MALICIOUS CODE HERE >
// now runs whenever the host maps over any array
};That's prototype pollution, and it's only half the problem. The Function constructor compiles a string into a live function with the privileges of whoever calls it. Get a reference to the host's Function, and you can write new code at runtime that runs on the host's side of the boundary.
This is why vm2 had so many separation failures. Trying to build a security boundary inside a single V8 context using JavaScript-level tricks like proxies, prototype scrubbing, and careful wrapping was an uphill battle. Guest and host shared one heap, one set of prototypes, and one Function constructor. Every escape was a variation on the same theme: finding a path back across a boundary that was an overlay, not a foundation.
Isolates give each side its own everything
V8 is the JavaScript engine in Chrome and Node.js. Its unit of separation is the Isolate: an instance of the JavaScript engine with its own heown garbage collector, own copy of the built-ins.
Two consequences follow, and between them they close both of the routes above.
Separate heaps mean there is no pointer from an object in the guest's graph to an object in the host's graph, and no way to manufacture one from JavaScript. "Walk the object graph until you find something useful" stops being a strategy because the graph terminates.
Separate built-ins mean the guest's Array.prototype is a genuinely different object from the host's. Redefining one has no effect on the other. The guest's Function constructor is a separate object, so calling it produces guest-side code in the guest's heap and nothing more.
This is the same primitive Chrome uses to prevent tabs from reading each other's memory, although, in fairness, Chrome does most of that work with OS process isolation and uses Isolates within it. The comparison is directionally right rather than exact.

isolated-vm, and what still has to cross
In Node.js, the package that gives you this is isolated-vm. It pulls in over a million downloads a week and turns up in a lot of places you'd expect: Rocket.Chat sandboxes integration scripts with it; Directus and Budibase run user-authored automation scripts in it, Mastra uses it for model-generated tool orchestration, and n8n recommends it for isolating Code-node scripts. Each sandbox gets its own Isolate, so guest code starts with no require, no host globals, and no reference to anything in the host process.
A sandbox that can't communicate, however, is just an expensive way to burn CPU. Something has to get in and out.
Here's where the design gets interesting. Because no object graph is shared, you cannot pass a reference the way you would between two functions in the same program. Values have to be copied: serialized on one side, rebuilt on the other. isolated-vm does this with ExternalCopy, and for cases where the host wants to deliberately expose a capability, it offers Reference, a handle the embedder hands across on purpose.
That copying is performed by C++ binding code that holds raw pointers into both heaps simultaneously. Which is exactly where the boundary got tested this year.
In August 2026, Endor Labs disclosed GHSA-864f-rcv7-6rh4, a critical flaw in that copy path. Guest code could get the host's own C++ to follow a value the guest controlled, escalating from a controlled crash to redirecting the host's execution. The Isolate held. V8's memory isolation was never defeated. The glue wrapped around it was. The maintainer fixed it quickly in 7.0.1 and 6.2.0.
Implications for your threat model
A few key takeaways:
The boundary is the Isolate plus every capability you hand across it. A single Reference is a capability, and in this case, one was enough to reach the vulnerable code. Audit what you expose, not just how you isolate.
Native binding code counts as an attack surface. A sound isolation primitive wrapped in memory-unsafe C++ is only as safe as that C++, and marshaling layers get far less scrutiny than the primitives they wrap.
Know your versions. If isolated-vm is in your dependency tree, and given the download numbers, there's a fair chance it is. Confirm you're on 7.0.1 or 6.2.0.
If you want the full technical breakdown of the vulnerability, including how a stateful getter turned a type check into a type confusion, our research team wrote it up here.
Additional resources
What's next?
When you're ready to take the next step in securing your software supply chain, here are 3 ways Endor Labs can help:








