🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

vm2

Package Overview
Dependencies
Maintainers
1
Versions
77
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

vm2 - npm Package Compare versions

Comparing version
3.11.3
to
3.11.4
+101
-18
lib/builtin.js

@@ -40,6 +40,7 @@

// SECURITY (GHSA-947f-4v7f-x2v8): Some Node builtins are sandbox-bypass primitives
// by design -- their primary capability is to reach host code regardless of the
// vm2 builtin allowlist. They must NEVER be reachable from the sandbox, even when
// the user requests `'*'` or explicitly names them in `builtin`.
// SECURITY (GHSA-947f-4v7f-x2v8, GHSA-rp36-8xq3-r6c4): Some Node builtins are
// sandbox-bypass primitives by design -- their primary capability is to reach
// host code regardless of the vm2 builtin allowlist. They must NEVER be
// reachable from the sandbox, even when the user requests `'*'` or explicitly
// names them in `builtin`.
//

@@ -59,3 +60,12 @@ // - module : exposes `Module._load`, `Module._resolveFilename`,

// - inspector : the inspector protocol can attach a debugger to the
// host process, exposing arbitrary host state.
// host process, exposing arbitrary host state. Covers
// the subpath family `inspector/promises` as well.
// - process : `process.getBuiltinModule(name)` (Node 22+) reloads
// ANY core module regardless of the embedder's
// allow/deny configuration. `process.binding`,
// `process.dlopen`, `process._linkedBinding`, and the
// raw host `process.env` are equally fatal. The
// sandbox global `process` is a sanitized shim defined
// in `setup-node-sandbox.js`; `require('process')`
// returns the raw host module and is never safe.
//

@@ -67,2 +77,10 @@ // This denylist is enforced at the `BUILTIN_MODULES` source (so the `'*'`

// these names if a user genuinely needs one.
//
// Matching is family-based: any builtin whose path is `<family>/...` where
// `<family>` is listed below is also blocked. This covers
// `inspector/promises` today and any future subpath such as
// `inspector/foo`, `process/foo`, `module/foo`. The `node:` URL-style
// prefix is stripped before matching so neither `require('node:process')`
// nor `require('node:inspector/promises')` can bypass via the alternative
// spelling.
const DANGEROUS_BUILTINS = new Set([

@@ -75,2 +93,3 @@ 'module',

'inspector',
'process',
// Host-process abort DoS: `trace_events.createTracing({categories: [...]})`

@@ -87,7 +106,68 @@ // asserts `args[0]->IsArray()` in C++; the array crosses the bridge as a

// controlled wrapper via `mock`/`override`.
'wasi'
'wasi',
// SECURITY (GHSA-9g8x-92q2-p28f): Process-wide observability builtins.
// Unlike most Node builtins, these expose state of the *entire host
// process* rather than sandbox-local state -- the vm2 boundary cannot
// usefully contain them because the data they surface (HTTP requests,
// async-context, perf marks, heap contents) belongs to the embedder.
// Even a readonly proxy that forwards every call to the host module is
// a working host-data exfiltration primitive:
//
// - diagnostics_channel : `dc.channel('http.server.request.start').subscribe(cb)`
// hands the sandbox raw host IncomingMessage
// objects -- including Authorization /
// session-token headers -- for every request the
// embedder receives.
// - async_hooks : `executionAsyncResource()` returns the host's
// current AsyncResource; embedders routinely
// pin per-request user/auth state on it via
// AsyncLocalStorage.
// - perf_hooks : `performance.getEntriesByType('mark')` reads
// every host-side `performance.mark(name)`,
// which embedders often label with request IDs,
// user IDs, or query strings.
// - v8 : `v8.getHeapSnapshot()` / `v8.writeHeapSnapshot()`
// serialize the *entire* host V8 heap (every
// string, every Buffer, every closure capture)
// and `v8.queryObjects(Ctor)` (Node 20+) returns
// every host-realm instance of a constructor.
// Strictly worse than perf_hooks for the same
// reason -- host process state, not sandbox state.
//
// Embedders who genuinely need a sandbox-local replacement can register a
// controlled wrapper under the same name via `mock` / `override`; the
// denylist only rejects the default host-passthrough loader.
'diagnostics_channel',
'async_hooks',
'perf_hooks',
'v8'
]);
// SECURITY (GHSA-rp36-8xq3-r6c4): Family-prefix denylist check. `inspector` and
// `inspector/promises` must share fate; same for any future subpath under a
// dangerous family. Also strips the `node:` URL-style prefix so
// `node:process` and `node:inspector/promises` cannot bypass via spelling.
function isDangerousBuiltin(key) {
if (typeof key !== 'string') return false;
if (key.startsWith('node:')) key = key.slice(5);
if (DANGEROUS_BUILTINS.has(key)) return true;
const slash = key.indexOf('/');
if (slash > 0 && DANGEROUS_BUILTINS.has(key.slice(0, slash))) return true;
return false;
}
// SECURITY (GHSA-r9pm-gxmw-wv6p): Underscored builtins (_http_client,
// _http_server, _http_agent, _http_common, _http_incoming, _http_outgoing,
// _tls_common, _tls_wrap, _stream_*) are Node's private implementation
// modules backing http/https/tls/streams. They are listed by
// `require('module').builtinModules` but are not documented public API and
// expose network primitives directly (`_http_client.ClientRequest`,
// `_http_server.Server`). Filtering them at the `BUILTIN_MODULES` source
// removes them from `'*'` wildcard expansion, so the documented
// `builtin: ['*', '-http', '-https', '-net', '-tls', ...]` pattern is
// once again coherent. Explicit opt-in (`builtin: ['_http_client']`) and
// `mock`/`override` registrations remain functional via `addDefaultBuiltin`
// -- power users who genuinely need an internal sibling can still name it.
const BUILTIN_MODULES = (nmod.builtinModules || Object.getOwnPropertyNames(process.binding('natives')))
.filter(s=>!s.startsWith('internal/') && !DANGEROUS_BUILTINS.has(s));
.filter(s=>!s.startsWith('internal/') && !s.startsWith('_') && !isDangerousBuiltin(s));

@@ -141,13 +221,16 @@ let EventEmitterReferencingAsyncResourceClass = null;

const special = SPECIAL_MODULES[key];
// SECURITY (GHSA-947f-4v7f-x2v8): Defense-in-depth. Reject sandbox-bypass
// primitives even when the caller explicitly names them (e.g.
// `builtin: ['module']` or `makeBuiltins(['worker_threads'])`). A non-special
// dangerous builtin would otherwise be wrapped in a readonly proxy whose
// `apply` trap forwards every method call to the host realm -- handing the
// sandbox a primitive that loads ANY other builtin (`Module._load`),
// spawns processes (`cluster.fork`), runs unsandboxed code
// (`new Worker(src, {eval:true})`), or evaluates host-realm code
// (`vm.runInThisContext`). The `SPECIAL_MODULES` escape hatch above is
// still honoured -- a future safe wrapper can be registered there.
if (!special && DANGEROUS_BUILTINS.has(key)) return;
// SECURITY (GHSA-947f-4v7f-x2v8, GHSA-rp36-8xq3-r6c4): Defense-in-depth.
// Reject sandbox-bypass primitives even when the caller explicitly names
// them (e.g. `builtin: ['module']`, `builtin: ['process']`,
// `makeBuiltins(['inspector/promises'])`). A non-special dangerous builtin
// would otherwise be wrapped in a readonly proxy whose `apply` trap
// forwards every method call to the host realm -- handing the sandbox a
// primitive that loads ANY other builtin (`Module._load`,
// `process.getBuiltinModule`), spawns processes (`cluster.fork`), runs
// unsandboxed code (`new Worker(src, {eval:true})`,
// `inspector/promises Session.post('Runtime.evaluate')`), or evaluates
// host-realm code (`vm.runInThisContext`). The `SPECIAL_MODULES` escape
// hatch above is still honoured -- a future safe wrapper can be
// registered there.
if (!special && isDangerousBuiltin(key)) return;
builtins.set(key, special ? special : vm => vm.readonly(hostRequire(key)));

@@ -154,0 +237,0 @@ }

+52
-22

@@ -253,24 +253,2 @@ 'use strict';

constructor(options = {}) {
// SECURITY (GHSA-8hg8-63c5-gwmx): `nesting: true` injects a NESTING_OVERRIDE
// builtin that exposes `vm2` to the sandbox regardless of `require: false`.
// The sandbox then constructs an inner NodeVM with attacker-chosen `require`
// config (unconstrained by the outer config — by design of nesting) and
// reaches `child_process` for full host RCE. The contradictory option pair
// is the specific trap the advisory describes; reject it at construction
// with a clear error rather than silently producing an unsandboxed config.
// (Bare `nesting: true` without `require: false` continues to work as the
// documented escape hatch; the README "`nesting: true` is an escape hatch"
// section explains the broader trade-off.)
if (options.nesting === true && options.require === false) {
throw new VMError(
'NodeVM `nesting: true` is incompatible with `require: false`. '
+ '`nesting: true` is an escape hatch that lets sandbox code '
+ '`require(\'vm2\')` and construct nested VMs unconstrained by the outer '
+ 'config — which contradicts `require: false`. To deny all requires, '
+ 'remove `nesting: true`. To allow nested VMs, replace `require: false` '
+ 'with an explicit config (e.g. `require: { builtin: [] }`) so the '
+ 'tradeoff is visible. See README "`nesting: true` is an escape hatch". '
+ 'Context: GHSA-8hg8-63c5-gwmx.'
);
}
const {

@@ -294,2 +272,54 @@ compiler,

// SECURITY (GHSA-m4wx-m65x-ghrr, supersedes GHSA-8hg8-63c5-gwmx):
// A truthy `nesting` injects a NESTING_OVERRIDE builtin that exposes
// `vm2` to the sandbox. When `requireOpts` is not a real require-config
// object, `makeResolverFromLegacyOptions` produces a resolver whose ONLY
// builtin is `vm2` — a pure escape primitive (sandbox does
// `require('vm2')`, builds an inner NodeVM with attacker-chosen
// `require` config, reaches `child_process` for host RCE).
//
// The guard mirrors the actual reachability of that insecure resolver
// on two axes:
//
// 1. `nesting` truthiness, not strict `=== true`. The override is gated
// a few lines below by `nesting && NESTING_OVERRIDE`, which fires for
// ANY truthy value (`1`, `'yes'`, `{}`, `[]`, etc.). The check must
// cover every truthy `nesting` value the override gate accepts.
//
// 2. `requireOpts` must be an actual require-config object (or a
// `Resolver` instance), not just "truthy". `makeResolverFromLegacyOptions`
// destructures every primitive/function value to all-`undefined` and
// falls into the same NESTING_OVERRIDE-only `if (!options)` branch as
// falsy values do. The shapes that collapse to the insecure resolver:
// - `require: false` / `undefined` / `null` / `0` / `''`
// → falsy → `if (!options)` branch
// - `require: true` / `1` / `'yes'` / `Symbol()` / `function(){}`
// → truthy non-object → destructured to all-undefined →
// `makeBuiltinsFromLegacyOptions(undefined, …, override)` →
// same NESTING_OVERRIDE-only resolver
// The documented escape hatch (a truthy `nesting` + an explicit
// `require` config object, even `{}`) continues to work — a non-null
// object counts as the developer's deliberate acknowledgment of the
// tradeoff. A custom `Resolver` instance is also accepted (the
// `customResolver` path below bypasses `makeResolverFromLegacyOptions`
// entirely, so NESTING_OVERRIDE is never injected into it).
const hasRealRequireConfig =
requireOpts instanceof Resolver
|| (typeof requireOpts === 'object' && requireOpts !== null);
if (nesting && !hasRealRequireConfig) {
throw new VMError(
'NodeVM `nesting` requires an explicit `require` config object. '
+ '`nesting` is an escape hatch that exposes `vm2` to the '
+ 'sandbox (sandbox code can `require(\'vm2\')` and construct nested '
+ 'VMs unconstrained by the outer config). With `require` set to '
+ 'anything other than a config object (`false`, omitted, `true`, '
+ 'a number, a string, a function), the resolver exposes ONLY '
+ '`vm2` — a pure escape primitive. To deny all requires, remove '
+ '`nesting`. To allow nested VMs, pass an explicit `require` '
+ 'config (e.g. `require: { builtin: [] }`) so the tradeoff is '
+ 'visible. See README "`nesting: true` is an escape hatch". '
+ 'Context: GHSA-m4wx-m65x-ghrr (supersedes GHSA-8hg8-63c5-gwmx).'
);
}
// Throw this early

@@ -296,0 +326,0 @@ if (sandbox && 'object' !== typeof sandbox) {

@@ -16,3 +16,3 @@ {

],
"version": "3.11.3",
"version": "3.11.4",
"main": "index.js",

@@ -19,0 +19,0 @@ "sideEffects": false,

@@ -521,3 +521,3 @@ # vm2 [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![License][license-image]][license-url] [![Node.js CI](https://github.com/patriksimek/vm2/actions/workflows/test.yml/badge.svg)](https://github.com/patriksimek/vm2/actions/workflows/test.yml) [![Known Vulnerabilities][snyk-image]][snyk-url]

The combination `{ nesting: true, require: false }` throws `VMError` at construction (GHSA-8hg8-63c5-gwmx) because the pair is contradictory: `nesting: true` makes `vm2` requireable regardless of `require: false`, so the deny-all expectation cannot be honored. To deny all requires, remove `nesting: true`. To allow nested VMs, replace `require: false` with an explicit config so the tradeoff is visible.
`nesting: true` **requires an explicit `require` config object** (e.g. `require: { builtin: [] }` or `require: {}`). Any other shape — `require: false`, `require: undefined`, `require: null`, or omitting `require` entirely — throws `VMError` at construction (GHSA-m4wx-m65x-ghrr, supersedes GHSA-8hg8-63c5-gwmx). All of those shapes produce a NESTING_OVERRIDE-only resolver: the sandbox can `require('vm2')` but nothing else, which is a pure escape primitive with no legitimate use. To deny all requires, remove `nesting: true`. To allow nested VMs, provide an explicit `require` config so the trade-off is visible at the call site.

@@ -524,0 +524,0 @@ ## Known Issues

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display