NewApp Monitoring is liveSign up free
AgentbeamBeta
Open-source package vulnerability · npm

CVE-2026-92954: vm2: Host-returned Promise rejection can bypass vm2's unhandled-rejection hardening and terminate the host process

criticalCVSS 4.0 9.2 · 3.1 8.6GHSA-gjq8-xm47-88rc

vm2 3.10.0 through 3.11.7 lets sandboxed code crash the host Node.js process. If sandbox code calls a host function that returns a rejected Promise and ignores the result, Node's default unhandled-rejection behavior kills the process. The earlier hardening only covered Promises created inside the sandbox. Upgrade to vm2 3.11.8.

Published Oct 5, 2026 · Analysis Oct 25, 2026

What an attacker can do

vm2 already swallows unhandled rejections for Promises that sandbox code creates, through a localPromise wrapper in lib/setup-sandbox.js. Promises that cross the bridge from the host are not marked handled. If the sandbox never attaches .catch() or .then(..., onRejected), the original host Promise stays orphaned. Node then raises it as an uncaught exception and exits.

Two paths are documented. In a VM, the embedder exposes a host function that can return a rejected Promise, for example hostReject: () => Promise.reject(new Error('host-boom')). In a NodeVM, the events builtin is on the allowlist. vm2's wrapper re-exports the host EventEmitter.once, so events.once(ee, 'message') followed by ee.emit('error', ...) yields a rejected host Promise.

The result is denial of service only. One small request can kill a worker that serves every user, in web services, queues, notebook workers or plugin hosts. A restart does not help, because the payload can be replayed. The reporter reproduced it on Node 16 to 25.

Who is affected

vm2 3.10.0 through 3.11.7 are affected, but only with a trigger configuration. Either the embedder exposes a host function that returns Promises to the sandbox, or a NodeVM allows the events builtin, as in require: { external: false, builtin: ['events'] }. A NodeVM without events and with no Promise-returning host helpers did not crash in the report's controls. Check your version and your options:

npm ls vm2

vm2

Ecosystem
npm
Affected
>= 3.10.0, <= 3.11.7
Fixed in
3.11.8
Check
npm ls vm2

How to fix it

Upgrade to vm2 3.11.8.

npm install [email protected]

The advisory also gives an application-level stopgap. Install a process-level unhandledRejection handler that swallows vm2-originated rejections. This is not a library fix. Without a handler, Node's default --unhandled-rejections=throw exits the process. Removing events from the NodeVM builtin allowlist closes the second path.

How to tell if you were hit

Look for unexplained host process exits right after sandbox runs, with a stack that starts at your host helper or reads triggerUncaughtException(err, true /* fromPromise */) from node:internal/process/promises. Repeated crashes after restart, tied to the same tenant or script, suggest replay. Review sandbox code for calls to host Promise-returning helpers or events.once( without .catch.

Original advisory

From the GitHub Advisory Database (CC BY 4.0).

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:

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:

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:

once: EventEmitter.once,

Then lib/events.js re-exports it:

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:

const vm = new VM({
  sandbox: {
    hostReject: () => Promise.reject(new Error('host-boom')),
  },
});

The NodeVM variant requires events in the builtin allowlist:

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:

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:

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:

Error: host-boom
    at hostReject (...)

The process exits before printing survived.

Minimal NodeVM builtin variant:

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:

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.

Credited to rexpository.

Questions

Does CVE-2026-92954 affect every vm2 user?

Not by default. The sandbox needs a way to get a rejected host Promise: an embedder-exposed async helper, or a NodeVM with the events builtin allowed. Without either, the report's controls show the process survives.

Is vm2 3.11.5 or 3.11.7 vulnerable?

Yes. The range is 3.10.0 through 3.11.7, and 3.11.8 is the fix.

Can an attacker run code on the host with this?

No. The advisory describes availability impact only, a host process crash. It has no confidentiality or integrity impact.

Is a workaround available?

Yes. Install a process-level unhandledRejection handler, or keep events off the NodeVM allowlist. Upgrading to 3.11.8 is the real fix.

References

How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology

More npm vulnerabilities

All npm entries