CVE-2026-92954: vm2: unhandled host Promise rejection crashes Node host
GHSA-gjq8-xm47-88rcHIGHPoC AVAILABLECISA: TRACK*vm2 versions 3.10.0 through 3.11.7 let untrusted sandbox code crash the entire host Node.js process. It does this by calling a host-provided function that returns a rejected Promise and ignoring the result, because the bridge never marks that original Promise as handled. The CVSS is 8.6 (availability only, no confidentiality or integrity impact), and the attack needs no authentication or user interaction. Exploitation likelihood is moderate to low: EPSS is 0.00488 (top 60%), the CVE is not in CISA KEV, and there is no public exploit or Nuclei template, though the advisory includes a trivial proof of concept. Blast radius is notable, since vm2 has 3,047 downstream dependents and is widely used to run LLM-generated or user-supplied code in agent platforms, notebooks and plugin hosts. The package also has 62 other CVEs and an OpenSSF Scorecard of 5.8/10, which signals chronic sandbox-escape and hardening debt. Upgrade to vm2 3.11.8 now. Until then, install a process-level unhandledRejection handler and avoid exposing Promise-returning host functions or the NodeVM events builtin to untrusted code.
What is the risk?
Overall risk is MEDIUM-HIGH for internet-facing code-execution services and LOW for internal-only use. Exploitation is trivial and needs no privileges, but the impact is limited to availability (CVSS A:H, C:N, I:N). Exposure depends on the embedder configuration: either a host function that returns a rejected Promise, or a NodeVM with the 'events' builtin allowed. The payload can be replayed after every restart, so automatic restarts do not remove the exposure. EPSS is low (0.00488), there is no KEV listing and no public exploit, which lowers near-term mass-exploitation odds. Targeted attacks on multi-tenant execution platforms are still realistic. A single request can take down a worker serving all tenants.
How does the attack unfold?
What systems are affected?
| Package | Ecosystem | Vulnerable Range | Patched |
|---|---|---|---|
| Jupyter Notebook | npm | >= 3.10.0, <= 3.11.7 | 3.11.8 |
Do you use Jupyter Notebook? You're affected.
How severe is it?
What is the attack surface?
What should I do?
1 step1) Upgrade vm2 to 3.11.8 or later (fixes the bridge by marking returned host Promises as handled). 2) Interim workaround: register a process.on('unhandledRejection') handler that swallows vm2-originated rejections, and treat it as a stopgap only. 3) Review embedder code and remove or wrap host functions exposed to the sandbox that return Promises, for example by attaching a .catch(() => {}) before returning. 4) For NodeVM, remove 'events' from the builtin allowlist unless it is required. 5) Run untrusted code in isolated worker processes or containers, so a crash affects only one job, and add per-tenant rate limiting. 6) Detection: monitor for unexpected Node process exits with uncaught-exception-from-promise traces, repeated crash-restart loops, and sandbox submissions that call events.once or async host helpers without handlers. 7) Run npm audit or an SCA scan across services to find transitive vm2 dependencies, and evaluate migrating off vm2 to a stronger isolation primitive such as isolated-vm or container sandboxes given the package's history of 62 CVEs.
What does CISA's SSVC say?
Source: CISA Vulnrichment (SSVC v2.0). Decision based on the CISA Coordinator decision tree.
How is it classified?
Which compliance frameworks are affected?
This CVE is relevant to:
Frequently Asked Questions
What is CVE-2026-92954?
vm2 versions 3.10.0 through 3.11.7 let untrusted sandbox code crash the entire host Node.js process. It does this by calling a host-provided function that returns a rejected Promise and ignoring the result, because the bridge never marks that original Promise as handled. The CVSS is 8.6 (availability only, no confidentiality or integrity impact), and the attack needs no authentication or user interaction. Exploitation likelihood is moderate to low: EPSS is 0.00488 (top 60%), the CVE is not in CISA KEV, and there is no public exploit or Nuclei template, though the advisory includes a trivial proof of concept. Blast radius is notable, since vm2 has 3,047 downstream dependents and is widely used to run LLM-generated or user-supplied code in agent platforms, notebooks and plugin hosts. The package also has 62 other CVEs and an OpenSSF Scorecard of 5.8/10, which signals chronic sandbox-escape and hardening debt. Upgrade to vm2 3.11.8 now. Until then, install a process-level unhandledRejection handler and avoid exposing Promise-returning host functions or the NodeVM events builtin to untrusted code.
Is CVE-2026-92954 actively exploited?
Proof-of-concept exploit code is publicly available for CVE-2026-92954, increasing the risk of exploitation.
How to fix CVE-2026-92954?
1) Upgrade vm2 to 3.11.8 or later (fixes the bridge by marking returned host Promises as handled). 2) Interim workaround: register a process.on('unhandledRejection') handler that swallows vm2-originated rejections, and treat it as a stopgap only. 3) Review embedder code and remove or wrap host functions exposed to the sandbox that return Promises, for example by attaching a .catch(() => {}) before returning. 4) For NodeVM, remove 'events' from the builtin allowlist unless it is required. 5) Run untrusted code in isolated worker processes or containers, so a crash affects only one job, and add per-tenant rate limiting. 6) Detection: monitor for unexpected Node process exits with uncaught-exception-from-promise traces, repeated crash-restart loops, and sandbox submissions that call events.once or async host helpers without handlers. 7) Run npm audit or an SCA scan across services to find transitive vm2 dependencies, and evaluate migrating off vm2 to a stronger isolation primitive such as isolated-vm or container sandboxes given the package's history of 62 CVEs.
What systems are affected by CVE-2026-92954?
This vulnerability affects the following AI/ML architecture patterns: agent frameworks, code interpreter sandboxes, workflow automation platforms, plugin hosts, multi-tenant code execution.
What is the CVSS score for CVE-2026-92954?
CVE-2026-92954 has a CVSS v3.1 base score of 8.6 (HIGH). The EPSS exploitation probability is 0.49%.
What is the AI security impact?
Affected AI Architectures
MITRE ATLAS Techniques
AML.T0029Denial of AI ServiceAML.T0049Exploit Public-Facing ApplicationAML.T0050Command and Scripting InterpreterCompliance Controls Affected
What are the technical details?
Original Advisory
## Summary vm2 current head (`v3.11.5`, commit `7a1f5100b96f48d34e0fe104ab37c0acc5944f92`) can still be used to terminate the host Node.js process when sandbox code calls a host-realm function that returns a rejected host Promise and then ignores the returned value. This is an incomplete-fix variant of the `GHSA-hw58-p9xv-2mjh` unhandled rejection hardening. The `localPromise` constructor now catches and consumes sandbox-created unhandled rejections, but host Promises returned across the bridge are not marked handled at the bridge boundary. If the sandbox does not attach `.catch()` or `.then(..., onRejected)`, Node's default unhandled rejection behavior terminates the host process. ## Technical Details `lib/setup-sandbox.js` hardens sandbox-created Promises by wrapping the executor and attaching a benign swallow tail: ```js apply(globalPromisePrototypeThen, this, [undefined, localPromiseSwallow]); ``` That only applies to `localPromise` instances created inside the sandbox. Host-returned Promises cross the membrane through the bridge apply path. The bridge wraps callbacks when sandbox code later calls `.then`, `.catch`, or `.finally` on a host Promise: ```js bridge.setHostPromiseSanitizers(e => handleException(from(e)), from); ``` However, if sandbox code ignores the returned host Promise, no host-side rejection handler is attached. The original host Promise remains unhandled and Node terminates the host process under the default unhandled-rejection behavior. `NodeVM` provides an in-repository example of this primitive through the special `events` builtin wrapper. `lib/builtin.js` passes host `EventEmitter.once` into the sandbox: ```js once: EventEmitter.once, ``` Then `lib/events.js` re-exports it: ```js if (host.once) module.exports.once = host.once; ``` Calling `events.once(ee, 'message')` and then emitting `error` on `ee` returns a rejected host Promise through that wrapper. If ignored by sandbox code, it terminates the host process. ## Impact An attacker who can run code in a vm2 sandbox can terminate the host Node.js process when the embedder exposes a host Promise-returning API, or when a `NodeVM` permits the `events` builtin. For web services, queues, notebook workers, plugin hosts, and multi-tenant code execution systems, a single small request can terminate the worker process. Restart policies do not fully mitigate the issue because the payload can be replayed after each restart. ## Affected Package/Versions Confirmed affected on Node.js `v25.8.0`: - `v3.10.0` - `v3.10.1` - `v3.10.2` - `v3.10.3` - `v3.10.4` - `v3.10.5` - `v3.11.0` - `v3.11.1` - `v3.11.2` - `v3.11.3` - `v3.11.4` - `v3.11.5` / current head `7a1f5100b96f48d34e0fe104ab37c0acc5944f92` The final PoV was also reproduced on current head with Node.js `v16.20.2`, `v18.20.8`, `v20.20.2`, `v22.22.3`, `v24.16.0`, and `v25.9.0` using `npx node@<major>`. The local workstation default Node.js `v25.8.0` also reproduces. ## Configuration Required The general VM PoV requires an embedder-exposed host function that can return a rejected host Promise: ```js const vm = new VM({ sandbox: { hostReject: () => Promise.reject(new Error('host-boom')), }, }); ``` The NodeVM variant requires `events` in the builtin allowlist: ```js new NodeVM({ require: { external: false, builtin: ['events'] } }); ``` `events.once()` is exposed by vm2's special `events` builtin wrapper. Node's official API documents `events.once()` as returning a Promise that rejects when the watched emitter emits `error` while waiting for another event. ## Controls - If sandbox code attaches `.catch(() => {})` to the returned host Promise, the process survives. - If sandbox code creates and ignores a sandbox-native rejected Promise, the process survives on current head. This confirms the `GHSA-hw58` localPromise hardening is active. - If sandbox code attaches `.catch(() => {})` to the `events.once()` Promise, the `NodeVM` process survives. - If `NodeVM` does not allow the `events` builtin, the `events.once()` variant does not run and the process survives. ## Disclosure Policy Fit vm2's security policy asks reporters not to open public issues and to submit This report is intended for that private route and includes the requested reproduction steps, affected versions, environment/configuration details, and impact. The affected range is within the supported `3.x` line. ## Local Proof of Concept Run from the `oss-zero-day-harness` directory: ```fish node submission-bundle/vm2-pov-test-host-promise-return-unhandled-rejection-dos/pov-host-promise-return-unhandled-rejection-dos.js ``` The crash-safe PoV executes each case in a child process. A vulnerable result has `status: 1` for the positive cases and `status: 0` for controls. Minimal VM positive case: ```js const { VM } = require('vm2'); const vm = new VM({ sandbox: { hostReject: () => Promise.reject(new Error('host-boom')), }, }); vm.run('hostReject(); 1'); setTimeout(() => console.log('survived'), 150); ``` Observed on current head: ```text Error: host-boom at hostReject (...) ``` The process exits before printing `survived`. Minimal NodeVM builtin variant: ```js const { NodeVM } = require('vm2'); const vm = new NodeVM({ require: { external: false, builtin: ['events'] }, }); vm.run(` const events = require('events'); const ee = new events.EventEmitter(); events.once(ee, 'message'); ee.emit('error', new Error('event-boom')); module.exports = 'returned'; `, 'events-pov.js'); setTimeout(() => console.log('survived'), 150); ``` Observed on current head: ```text node:internal/process/promises:332 triggerUncaughtException(err, true /* fromPromise */); Error: event-boom ``` The process exits with status `1`. ## Mitigation Applications can reduce exposure by installing a process-level `unhandledRejection` handler that swallows vm2-originated rejections, as the README recommends for related async rejection caveats. That is an application workaround, not a library-level fix: without such a handler, current Node's default `--unhandled-rejections=throw` behavior raises the rejection as an uncaught exception and exits the process. ## Suggested Fix Direction When a host function call returns a host-realm Promise across the bridge into sandbox code, attach a benign host-side rejection handler to the raw returned Promise before wrapping it for the sandbox. This should mark the original host Promise handled without changing the value returned to sandbox code or hiding the rejection from sandbox code that later attaches its own `.catch()` / `.then(..., onRejected)`. Regression tests should cover: - `VM` with `hostReject: () => Promise.reject(new Error(...))`; calling `hostReject()` without `.catch()` must not terminate the process. - The same call with a sandbox `.catch()` must still deliver a sanitized rejection to the sandbox callback. - `NodeVM` with `require.builtin: ['events']`; calling `events.once(ee, 'message')` and then emitting `error` without `.catch()` must not terminate the process. - The same `events.once()` call with a sandbox `.catch()` must continue to deliver a sanitized rejection to the sandbox callback. - Sandbox-created rejected Promises should continue to be consumed by the existing localPromise hardening. ## Why This Is Not Intended Behavior vm2 already treats this failure mode as security-relevant. `GHSA-hw58-p9xv-2mjh` was assigned High severity for a sandbox-created unhandled rejection that terminated the host process, and current `setup-sandbox.js` explicitly states that the local Promise swallow tail exists so the host's `unhandledRejection` event never fires. This report shows the same availability boundary failure still exists for host-returned Promises: - the sandbox does not need `child_process`, filesystem, network, `nesting`, or dangerous builtins; - the `VM` variant needs only a common embedder pattern: exposing an async host helper to untrusted code; - the `NodeVM` variant needs only the documented `events` builtin allowlist; - a single sandbox call terminates the whole host process serving all users. This is distinct from the documented caveat that `timeout` cannot stop CPU loops. It is also distinct from the README's current async-function / `await using` caveat: this report does not require sandbox `async` syntax, async functions, disposable stacks, or V8 stack-formatting behavior. The issue is specifically that vm2's own bridge returns a host Promise to the sandbox without marking the host Promise handled, while the sandbox-created Promise path does mark rejections handled. This is also distinct from host-Promise callback sanitizer escapes. In those chains, sandbox code attaches a callback to a host Promise and then receives or returns a mis-sanitized value. Here, no sandbox Promise callback is required at all; the original host Promise is simply left orphaned after crossing the bridge.
Exploitation Scenario
An attacker submits a small snippet to an AI agent platform that runs LLM-generated or user-supplied JavaScript in a vm2 sandbox and exposes an async host helper (for example a fetch or tool-call wrapper). The snippet calls the helper with arguments that make it reject, such as an invalid URL, and discards the returned Promise. The rejected host Promise has no handler, so Node's default unhandled-rejection behavior raises an uncaught exception and kills the worker. The attacker replays the request after each automatic restart, keeping the agent backend down for all tenants. In a NodeVM deployment with the 'events' builtin, the same effect comes from calling events.once(ee, 'x') and then emitting 'error' on the emitter. The same payload could also be delivered indirectly through prompt injection that persuades an agent to generate and run the crashing code.
Weaknesses (CWE)
CWE-248 — Uncaught Exception: An exception is thrown from a function, but it is not caught.
Source: MITRE CWE corpus.
CVSS Vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:N/A:HReferences
- github.com/advisories/GHSA-gjq8-xm47-88rc
- github.com/patriksimek/vm2/commit/5bb37f8b3f0498675a71cfa4df2b408d8ef7d4fb
- github.com/patriksimek/vm2/releases/tag/v3.11.8
- github.com/patriksimek/vm2/security/advisories/GHSA-gjq8-xm47-88rc
- nvd.nist.gov/vuln/detail/CVE-2026-92954
- vulncheck.com/advisories/vm2-3.10.0-through-3.11.5-denial-of-service-via-host-promise
Timeline
Related Vulnerabilities
CVE-2026-7281110.0SiYuan: SQL injection enables cross-notebook DB access
Same package: notebookCVE-2026-6908510.0SiYuan: SQL injection in searchDocs allows DB tampering
Same package: notebookCVE-2026-6908410.0SiYuan: SQL injection in search endpoint exposes notebooks
Same package: notebookCVE-2026-6908310.0SiYuan: unauthenticated SQLi in full-text search endpoint
Same package: notebookCVE-2026-11105710.0Mercury: regex blocklist bypass allows unauthenticated RCE
Same package: notebook