+101
-18
@@ -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) { |
+1
-1
@@ -16,3 +16,3 @@ { | ||
| ], | ||
| "version": "3.11.3", | ||
| "version": "3.11.4", | ||
| "main": "index.js", | ||
@@ -19,0 +19,0 @@ "sideEffects": false, |
+1
-1
@@ -521,3 +521,3 @@ # vm2 [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![License][license-image]][license-url] [](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
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
379369
9.42%8938
6.8%