🚀 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.10.5
to
3.11.0
+60
-2
lib/builtin.js

@@ -40,7 +40,54 @@

const BUILTIN_MODULES = (nmod.builtinModules || Object.getOwnPropertyNames(process.binding('natives'))).filter(s=>!s.startsWith('internal/'));
// 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`.
//
// - module : exposes `Module._load`, `Module._resolveFilename`,
// `Module._cache`, `createRequire` -- loads ANY host
// builtin or external module bypassing the allowlist.
// - worker_threads : `new Worker(code, {eval: true})` runs arbitrary JS in
// a fresh thread that has no vm2 sandbox at all.
// - cluster : `cluster.fork()` spawns a host child process running
// attacker-controlled code.
// - vm : `vm.runInThisContext` evaluates code in the host realm,
// bypassing every bridge proxy.
// - repl : `repl.start()` constructs an interactive evaluator
// attached to host streams; low utility for sandboxed
// code, high host-RCE potential.
// - inspector : the inspector protocol can attach a debugger to the
// host process, exposing arbitrary host state.
//
// This denylist is enforced at the `BUILTIN_MODULES` source (so the `'*'`
// wildcard never expands to them) AND inside `addDefaultBuiltin` (so explicit
// `builtin: ['module']` / `makeBuiltins(['module'])` requests are rejected).
// `SPECIAL_MODULES` and `overrides` can still register safe replacements under
// these names if a user genuinely needs one.
const DANGEROUS_BUILTINS = new Set([
'module',
'worker_threads',
'cluster',
'vm',
'repl',
'inspector',
// Host-process abort DoS: `trace_events.createTracing({categories: [...]})`
// asserts `args[0]->IsArray()` in C++; the array crosses the bridge as a
// Proxy, which fails the assertion and aborts the entire host process.
// Reachable as ~150 bytes from sandbox under `builtin: ['*']`.
'trace_events',
// `wasi` exposes the WebAssembly System Interface preview1 syscall
// surface (filesystem `preopens`, host clock/random, network if
// preopened). API is experimental and broad; even a misconfigured
// `preopens: {}` exposes the host CWD when sandbox code constructs
// a WASI module. Embedders who genuinely need WASI can register a
// controlled wrapper via `mock`/`override`.
'wasi'
]);
const BUILTIN_MODULES = (nmod.builtinModules || Object.getOwnPropertyNames(process.binding('natives')))
.filter(s=>!s.startsWith('internal/') && !DANGEROUS_BUILTINS.has(s));
let EventEmitterReferencingAsyncResourceClass = null;
if (EventEmitter.EventEmitterAsyncResource) {
// eslint-disable-next-line global-require
const {AsyncResource} = require('async_hooks');

@@ -90,2 +137,13 @@ const kEventEmitter = Symbol('kEventEmitter');

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;
builtins.set(key, special ? special : vm => vm.readonly(hostRequire(key)));

@@ -92,0 +150,0 @@ }

+2
-2

@@ -20,3 +20,3 @@ 'use strict';

// ignoreWarnings[].message = /Can't resolve 'coffee-script'/
/* eslint-disable-next-line global-require */
const coffeeScript = require('coffee-script');

@@ -44,3 +44,3 @@ return (code, filename) => {

// ignoreWarnings[].message = /Can't resolve 'typescript'/
/* eslint-disable-next-line global-require */
const typescript = require('typescript');

@@ -47,0 +47,0 @@ return (code, filename) => {

@@ -674,3 +674,3 @@ // Copyright Joyent, Inc. and other Node contributors.

// No error code for this since it is a Warning
// eslint-disable-next-line no-restricted-syntax
const w = new Error('Possible EventEmitter memory leak detected. ' +

@@ -677,0 +677,0 @@ `${existing.length} ${String(type)} listeners ` +

@@ -12,2 +12,10 @@ 'use strict';

// SECURITY: Dereferences symlinks before path-prefix root checks.
// Without this, an attacker can place a symlink inside `require.root`
// pointing outside it; the lexical resolve() passes the prefix check
// while Node's native require() follows the symlink. See GHSA-cp6g-6699-wx9c.
realpath(path) {
return fs.realpathSync(path);
}
isSeparator(char) {

@@ -54,2 +62,9 @@ return char === '/' || char === pa.sep;

// SECURITY: See DefaultFileSystem.realpath. Custom fs modules that omit
// realpathSync will surface the missing-method error here, which the
// resolver translates into a deny-by-default. GHSA-cp6g-6699-wx9c.
realpath(path) {
return this.fs.realpathSync(path);
}
isSeparator(char) {

@@ -56,0 +71,0 @@ return char === '/' || char === this.path.sep;

@@ -258,3 +258,6 @@ 'use strict';

strict = false,
sandbox
sandbox,
timeout,
allowAsync,
bufferAllocLimit
} = options;

@@ -267,3 +270,7 @@

super({__proto__: null, compiler: compiler, eval: allowEval, wasm});
// SECURITY (GHSA-6785-pvv7-mvg7): forward bufferAllocLimit so embedders
// using NodeVM (the more common form for module-loading sandboxes) can
// opt into the cap. Also forward timeout / allowAsync which were
// previously silently dropped by the super() call.
super({__proto__: null, compiler: compiler, eval: allowEval, wasm, timeout, allowAsync, bufferAllocLimit});

@@ -270,0 +277,0 @@ const customResolver = requireOpts instanceof Resolver;

@@ -22,3 +22,3 @@ 'use strict';

// Set module.parser.javascript.commonjsMagicComments=true in your webpack config.
// eslint-disable-next-line global-require
return require(/* webpackIgnore: true */ moduleName);

@@ -55,7 +55,19 @@ }

isPathAllowed(filename) {
return this.rootPaths === undefined || this.rootPaths.some(path => {
if (!filename.startsWith(path)) return false;
if (this.rootPaths === undefined) return true;
// SECURITY: Dereference symlinks before the prefix check. The lexical
// resolve() does not follow symlinks but Node's native require() does,
// so a symlink inside the root pointing outside it would otherwise
// bypass the boundary. Deny by default if the path can't be canonicalized
// (missing file, broken link, or fs without realpath). GHSA-cp6g-6699-wx9c.
let realFilename;
try {
realFilename = this.fs.realpath(filename);
} catch (e) {
return false;
}
return this.rootPaths.some(path => {
if (!realFilename.startsWith(path)) return false;
const len = path.length;
if (filename.length === len || (len > 0 && this.fs.isSeparator(path[len-1]))) return true;
return this.fs.isSeparator(filename[len]);
if (realFilename.length === len || (len > 0 && this.fs.isSeparator(path[len-1]))) return true;
return this.fs.isSeparator(realFilename[len]);
});

@@ -220,3 +232,34 @@ }

const checkedRootPaths = rootPaths ? (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => fsOpt.resolve(f)) : undefined;
// SECURITY: Canonicalize root paths so the prefix comparison in isPathAllowed
// matches the realpath of candidate filenames. GHSA-cp6g-6699-wx9c.
//
// Eager FileSystem contract probe: if `require.root` is set the adapter
// MUST be able to dereference symlinks, otherwise the boundary degrades to
// a lexical prefix check (the exact CWE-59 condition the fix closes). Fail
// loudly at construction so users can fix their adapter, instead of silently
// denying every require() later.
let checkedRootPaths;
if (rootPaths !== undefined) {
if (typeof fsOpt.realpath !== 'function') {
throw new VMError('NodeVM `require.root` requires the FileSystem adapter to implement realpath(path). See lib/filesystem.js for the contract. Context: GHSA-cp6g-6699-wx9c.');
}
checkedRootPaths = (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => {
const resolved = fsOpt.resolve(f);
try {
return fsOpt.realpath(resolved);
} catch (e) {
// TypeError = adapter wired up realpath() but its underlying
// implementation (e.g. VMFileSystem's `fs.realpathSync`) is
// missing. Contract violation — surface it now instead of
// deny-by-default at every later require().
if (e instanceof TypeError) {
throw new VMError('NodeVM `require.root` realpath probe failed: ' + e.message + '. If using VMFileSystem with a custom fs module, the underlying fs must provide realpathSync. Context: GHSA-cp6g-6699-wx9c.');
}
// Other errors (ENOENT, EACCES) may legitimately occur if the
// root doesn't exist yet at construction. Fall back to lexical;
// isPathAllowed() still realpaths candidates at require() time.
return resolved;
}
});
}

@@ -223,0 +266,0 @@ const pathContext = typeof context === 'function' ? context : (() => context);

@@ -90,3 +90,3 @@ 'use strict';

return {
// eslint-disable-next-line quote-props
__proto__: null,

@@ -701,3 +701,3 @@ '.js': this.makeExtensionHandler(vm, 'loadJS'),

try {
// eslint-disable-next-line no-new
new URL(target);

@@ -704,0 +704,0 @@ isURL = true;

@@ -166,3 +166,3 @@ 'use strict';

// We do it this way so that there are no more arguments in the function.
// eslint-disable-next-line prefer-rest-params
useOptions = arguments[2] || {__proto__: null};

@@ -169,0 +169,0 @@ useFileName = options || useOptions.filename;

@@ -169,3 +169,3 @@ /* global host, data, VMError */

function createRequireForModule(mod) {
// eslint-disable-next-line no-shadow
function require(id) {

@@ -213,3 +213,3 @@ return requireImpl(mod, id, true);

// This is a function and not an arrow function, since the original is also a function
// eslint-disable-next-line no-shadow
global.setTimeout = function setTimeout(callback, delay, ...args) {

@@ -233,3 +233,3 @@ if (typeof callback !== 'function') throw new LocalTypeError('"callback" argument must be a function');

// eslint-disable-next-line no-shadow
global.setInterval = function setInterval(callback, interval, ...args) {

@@ -253,3 +253,3 @@ if (typeof callback !== 'function') throw new LocalTypeError('"callback" argument must be a function');

// eslint-disable-next-line no-shadow
global.setImmediate = function setImmediate(callback, ...args) {

@@ -273,3 +273,3 @@ if (typeof callback !== 'function') throw new LocalTypeError('"callback" argument must be a function');

// eslint-disable-next-line no-shadow
global.clearTimeout = function clearTimeout(timeout) {

@@ -279,3 +279,3 @@ clearTimer(timeout);

// eslint-disable-next-line no-shadow
global.clearInterval = function clearInterval(interval) {

@@ -285,3 +285,3 @@ clearTimer(interval);

// eslint-disable-next-line no-shadow
global.clearImmediate = function clearImmediate(immediate) {

@@ -327,3 +327,3 @@ clearTimer(immediate);

*/
// eslint-disable-next-line no-shadow
function process() {

@@ -330,0 +330,0 @@ return this;

@@ -13,8 +13,6 @@ /* global host, bridge, data, context */

Function: localFunction,
eval: localEval
eval: localEval,
} = global;
const {
freeze: localObjectFreeze
} = localObject;
const { freeze: localObjectFreeze } = localObject;

@@ -30,3 +28,3 @@ const {

getOwnPropertyDescriptor: localReflectGetOwnPropertyDescriptor,
ownKeys: localReflectOwnKeys
ownKeys: localReflectOwnKeys,
} = localReflect;

@@ -40,4 +38,76 @@

const globalPromise = global.Promise;
class localPromise extends globalPromise {}
// SECURITY (GHSA-hw58-p9xv-2mjh): cache the host then() before the
// `globalPromise.prototype.then` override below replaces it. The internal
// swallow tail attached in the localPromise constructor must use the
// unmodified host then() so it doesn't recurse through our own override
// (which calls resetPromiseSpecies and could throw on hostile prototypes).
const globalPromisePrototypeThen = globalPromise.prototype.then;
function localPromiseSwallow() { /* no-op consumer to silence unhandledRejection */ }
// SECURITY (GHSA-hw58-p9xv-2mjh): re-entrancy guard. Attaching the swallow
// tail invokes the native then() which constructs a downstream promise via
// the species protocol — that downstream construction would recurse back
// into this constructor. We only need a tail on the *outermost* user-
// visible promise; internal species constructions are left bare.
let localPromiseInSwallowTail = false;
class localPromise extends globalPromise {
// SECURITY (GHSA-hw58-p9xv-2mjh): wrap the user-supplied executor so any
// synchronous throw — including V8-internal throws produced while the
// engine is *inside* the executor (e.g. `e.name = Symbol(); e.stack`
// triggers FormatStackTrace -> host TypeError) — is funnelled through
// handleException and surfaces as a sandbox-realm rejection rather than
// a raw host-realm error. The swallow tail below additionally consumes
// the rejection if no sandbox `.catch()` is attached, so the host's
// `unhandledRejection` event never fires and Node 15+'s default-throw
// behaviour cannot be used to crash the host process.
constructor(executor) {
// Preserve native semantics: a non-callable executor must cause the
// Promise constructor to throw a TypeError synchronously. Calling
// super(executor) directly delegates that check to the native
// Promise constructor.
if (typeof executor !== 'function') {
super(executor);
return;
}
super(function wrappedExecutor(resolve, reject) {
try {
return apply(executor, this, [resolve, reject]);
} catch (e) {
// SECURITY: handleException walks SuppressedError /
// AggregateError sub-error chains and routes the value
// through ensureThis, so a sandbox `.catch()` handler sees
// a sandbox-realm value rather than a raw host TypeError.
reject(handleException(e));
}
});
// SECURITY: even after the rejection has been sanitised, if no
// sandbox code attaches a `.catch()` the host fires its
// unhandledRejection hook and (Node 15+ default) terminates the
// process. Attach a benign tail handler that consumes the rejection
// silently. The tail uses the *original* host then() (cached above)
// so it bypasses our own then() override and doesn't recurse.
if (!localPromiseInSwallowTail) {
localPromiseInSwallowTail = true;
try {
apply(globalPromisePrototypeThen, this, [undefined, localPromiseSwallow]);
} catch (e) {
// best effort — never let the swallow itself crash the executor
} finally {
localPromiseInSwallowTail = false;
}
}
}
}
// V8 creates async function promises using the realm's intrinsic Promise
// (globalPromise), not localPromise. Since localPromise.prototype is not
// in globalPromise instances' prototype chain, `p instanceof Promise`
// would return false without this. Delegate to globalPromise's instanceof
// which is safe because globalPromise is frozen later (line 826).
localReflectDefineProperty(localPromise, Symbol.hasInstance, {
__proto__: null,
value: function(instance) {
return instance instanceof globalPromise;
}
});
/*

@@ -63,3 +133,3 @@ * Symbol.for protection

Symbol.for = function(key) {
Symbol.for = function (key) {
// Convert to string once to prevent toString/toPrimitive bypass and TOCTOU attacks

@@ -104,3 +174,3 @@ const keyStr = '' + key;

enumerable: true,
configurable: true
configurable: true,
});

@@ -123,3 +193,3 @@ }

enumerable: true,
configurable: true
configurable: true,
});

@@ -171,3 +241,3 @@ }

const resetPromiseSpecies = (p) => {
const resetPromiseSpecies = p => {
// Note: We do not use instanceof to check if p is a Promise because

@@ -189,3 +259,8 @@ // Reflect.construct(Promise, [...], FakeNewTarget) can create a real Promise

try {
success = localReflectDefineProperty(p, 'constructor', { __proto__: null, value: localPromise, writable: true, configurable: true });
success = localReflectDefineProperty(p, 'constructor', {
__proto__: null,
value: localPromise,
writable: true,
configurable: true,
});
} catch (e) {

@@ -209,3 +284,8 @@ // If defineProperty throws (e.g., Proxy with throwing trap), treat as failure

onFulfilled = function onFulfilled(value) {
value = ensureThis(value);
// SECURITY (GHSA-mpf8-4hx2-7cjg): use `from` rather than `ensureThis`.
// ensureThis returns the raw `other` when no proto-mapping is found,
// so unmapped host objects (e.g., objects with `__proto__: null`)
// crossed into the sandbox callback unwrapped. `from` always returns
// a bridge proxy regardless of proto-mapping.
value = from(value);
return apply(origOnFulfilled, this, [value]);

@@ -241,5 +321,3 @@ };

const {
isArray: localArrayIsArray
} = localArray;
const { isArray: localArrayIsArray } = localArray;

@@ -255,17 +333,33 @@ const {

VMError,
ReadOnlyMockHandler
// SECURITY (GHSA-v37h-5mfm-c47c): token-bound handler factories. The
// bridge no longer exposes ReadOnlyMockHandler as a direct constructor;
// setup-sandbox must go through these helpers so the construction token
// (closure-scoped inside bridge.js) stays out of reach of sandbox code.
createReadOnlyMockHandler,
newBufferHandler,
rebindHandlerConstructor,
} = bridge;
const {
allowAsync,
GeneratorFunction,
AsyncFunction,
AsyncGeneratorFunction
} = data;
const { allowAsync, GeneratorFunction, AsyncFunction, AsyncGeneratorFunction, bufferAllocLimit } = data;
const {
get: localWeakMapGet,
set: localWeakMapSet
} = LocalWeakMap.prototype;
// SECURITY (GHSA-6785-pvv7-mvg7): Buffer.alloc / allocUnsafe / allocUnsafeSlow
// (and the deprecated Buffer(N) / new Buffer(N) forms) execute as a single
// synchronous host C++ allocation. V8's `timeout` cannot interrupt them, so
// an attacker controlling the size argument can amplify a small payload into
// hundreds of megabytes of host RSS, crashing the host process in
// memory-constrained environments (Docker/K8s/Lambda). Cap every allocation
// size before it reaches the host implementation. Cached in a const so a
// sandbox-side prototype-pollution attempt cannot mutate it post-init.
const localBufferAllocLimit = bufferAllocLimit;
function checkBufferAllocLimit(size) {
// Match host Buffer.alloc semantics: it expects a number. Non-numeric
// values are passed through to host validation (it throws TypeError).
// Only enforce the cap on numbers actually large enough to trip it.
if (typeof size === 'number' && size > localBufferAllocLimit) {
throw new RangeError('Buffer allocation size ' + size + ' exceeds bufferAllocLimit ' + localBufferAllocLimit);
}
}
const { get: localWeakMapGet, set: localWeakMapSet } = LocalWeakMap.prototype;
function localUnexpected() {

@@ -279,9 +373,9 @@ return new VMError('Should not happen');

Object.defineProperties(global, {
global: {value: global, writable: true, configurable: true, enumerable: true},
globalThis: {value: global, writable: true, configurable: true},
GLOBAL: {value: global, writable: true, configurable: true},
root: {value: global, writable: true, configurable: true},
Error: {value: LocalError},
Promise: {value: localPromise},
Proxy: {value: undefined}
global: { value: global, writable: true, configurable: true, enumerable: true },
globalThis: { value: global, writable: true, configurable: true },
GLOBAL: { value: global, writable: true, configurable: true },
root: { value: global, writable: true, configurable: true },
Error: { value: LocalError },
Promise: { value: localPromise },
Proxy: { value: undefined },
});

@@ -309,19 +403,33 @@

if (!localReflectDefineProperty(global, 'VMError', {
__proto__: null,
value: VMError,
writable: true,
enumerable: false,
configurable: true
})) throw localUnexpected();
if (
!localReflectDefineProperty(global, 'VMError', {
__proto__: null,
value: VMError,
writable: true,
enumerable: false,
configurable: true,
})
)
throw localUnexpected();
// Fixes buffer unsafe allocation
/* eslint-disable no-use-before-define */
class BufferHandler extends ReadOnlyHandler {
// SECURITY (GHSA-v37h-5mfm-c47c): forward every arg (token + object)
// to super() so BaseHandler's token check succeeds. Without this
// forward, or if the constructor is reached by sandbox code without
// the token, the super() call throws and no BufferHandler instance
// is produced.
constructor(...args) {
super(...args);
}
apply(target, thiz, args) {
if (args.length > 0 && typeof args[0] === 'number') {
// SECURITY (GHSA-6785-pvv7-mvg7): deprecated Buffer(N) form. Cap before delegating to host.
checkBufferAllocLimit(args[0]);
return LocalBuffer.alloc(args[0]);
}
return localReflectApply(LocalBuffer.from, LocalBuffer, args);
return apply(LocalBuffer.from, LocalBuffer, args);
}

@@ -331,23 +439,50 @@

if (args.length > 0 && typeof args[0] === 'number') {
// SECURITY (GHSA-6785-pvv7-mvg7): deprecated new Buffer(N) form. Cap before delegating.
checkBufferAllocLimit(args[0]);
return LocalBuffer.alloc(args[0]);
}
return localReflectApply(LocalBuffer.from, LocalBuffer, args);
return apply(LocalBuffer.from, LocalBuffer, args);
}
}
/* eslint-enable no-use-before-define */
const LocalBuffer = fromWithFactory(obj => new BufferHandler(obj), host.Buffer);
// SECURITY (post-GHSA-v37h hardening): rebind BufferHandler.prototype.constructor
// to the throw-always sentinel so `Object.getPrototypeOf(handler).constructor`
// on a leaked BufferHandler returns the sentinel rather than the real subclass.
// Layer 1 (token check via super(...args)) already blocks the actual construction,
// but Layer 3 was advertised as "every handler prototype" while only covering the
// four core classes — this closes the gap for handler subclasses defined outside
// bridge.js.
rebindHandlerConstructor(BufferHandler);
// SECURITY (GHSA-v37h-5mfm-c47c): construction goes through
// newBufferHandler, which injects the closure-scoped construction token.
const LocalBuffer = fromWithFactory(obj => newBufferHandler(BufferHandler, obj), host.Buffer);
if (!localReflectDefineProperty(global, 'Buffer', {
__proto__: null,
value: LocalBuffer,
writable: true,
enumerable: false,
configurable: true
})) throw localUnexpected();
if (
!localReflectDefineProperty(global, 'Buffer', {
__proto__: null,
value: LocalBuffer,
writable: true,
enumerable: false,
configurable: true,
})
)
throw localUnexpected();
addProtoMapping(LocalBuffer.prototype, host.Buffer.prototype, 'Uint8Array');
// SECURITY (GHSA-6785-pvv7-mvg7): cap Buffer.alloc before delegating to host.
// The captured `localBufferAllocOriginal` is the bridge proxy of host.Buffer.alloc;
// `connect()` then registers our wrapper as the canonical sandbox-side alloc, so
// future sandbox lookups of `Buffer.alloc` route through the cap.
const localBufferAllocOriginal = LocalBuffer.alloc;
function alloc(size, fill, encoding) {
checkBufferAllocLimit(size);
// Use raw Reflect.apply (`apply`) here — LocalBuffer is a frozen bridge proxy.
return apply(localBufferAllocOriginal, LocalBuffer, arguments);
}
connect(alloc, host.Buffer.alloc);
/**

@@ -360,2 +495,6 @@ *

function allocUnsafe(size) {
// SECURITY (GHSA-6785-pvv7-mvg7): cap before delegating. LocalBuffer.alloc
// is already capped via connect() above, but we check here too so a future
// refactor cannot silently re-open this path.
checkBufferAllocLimit(size);
return LocalBuffer.alloc(size);

@@ -373,2 +512,4 @@ }

function allocUnsafeSlow(size) {
// SECURITY (GHSA-6785-pvv7-mvg7): cap before delegating (see allocUnsafe).
checkBufferAllocLimit(size);
return LocalBuffer.alloc(size);

@@ -392,3 +533,5 @@ }

const remaining = this.length - max;
let str = this.hexSlice(0, actualMax).replace(/(.{2})/g, '$1 ').trim();
let str = this.hexSlice(0, actualMax)
.replace(/(.{2})/g, '$1 ')
.trim();
if (remaining > 0) str += ` ... ${remaining} more byte${remaining > 1 ? 's' : ''}`;

@@ -413,2 +556,68 @@ return `<${this.constructor.name} ${str}>`;

/*
* Safe default prepareStackTrace function.
*
* When Error.prepareStackTrace is undefined in the sandbox, V8 falls back to
* Node.js's host-side prepareStackTraceCallback (from node:internal/errors).
* If that host code throws (e.g., when error.name is a Symbol), the TypeError
* is a host-realm error, which can be used for sandbox escape.
*
* This function ensures V8 never falls back to the host formatter. It safely
* handles Symbol names, Proxy objects, and other exotic types without throwing.
*/
function defaultSandboxPrepareStackTrace(error, callSites) {
// Safely convert error to a header string, handling Symbol names,
// Proxy objects, and other exotic types that would throw during coercion.
let header;
try {
let name;
try {
name = error.name;
} catch (e) {
name = 'Error';
}
// If name is a Symbol or other non-string, safely coerce it
if (typeof name === 'symbol') {
try {
name = name.toString();
} catch (e) {
name = 'Error';
}
} else if (typeof name !== 'string') {
try {
name = '' + name;
} catch (e) {
name = 'Error';
}
}
let message;
try {
message = error.message;
} catch (e) {
message = '';
}
if (typeof message !== 'string') {
try {
message = '' + message;
} catch (e) {
message = '';
}
}
header = message ? name + ': ' + message : name;
} catch (e) {
header = 'Error';
}
// Format each call site safely
const lines = [header];
for (let i = 0; i < callSites.length; i++) {
try {
lines[lines.length] = ' at ' + callSites[i];
} catch (e) {
lines[lines.length] = ' at <error formatting frame>';
}
}
return lines.join('\n');
}
let currentPrepareStackTrace = LocalError.prepareStackTrace;

@@ -419,2 +628,8 @@ const wrappedPrepareStackTrace = new LocalWeakMap();

}
// HARDENING (post-#563): the original PR pre-registered defaultSandboxPrepareStackTrace
// in the WeakMap as identity (mapping itself to itself), which would have caused the
// setter to bypass the call-site wrapping path. Removed — the setter now wraps the
// default through the same `newWrapped` logic as user-provided functions, so callsite
// `toString()` invocations go through the sandbox `CallSite` wrapper class and don't
// leak host paths.

@@ -427,2 +642,8 @@ let OriginalCallSite;

if (typeof OriginalCallSite === 'function') {
// SECURITY (GHSA-v27g-jcqj-v8rw): if we leave prepareStackTrace as
// `undefined`, V8 falls through to its native default formatter, which
// emits absolute host paths and host function names into `error.stack`.
// Defer the install of our sandbox default until OriginalCallSite-based
// frame classification is available below; for now, set to undefined so
// the setter installed later can take over.
LocalError.prepareStackTrace = undefined;

@@ -432,11 +653,17 @@

const callSiteGetters = [];
for (let i=0; i<list.length; i++) {
for (let i = 0; i < list.length; i++) {
const name = list[i];
const func = OriginalCallSite.prototype[name];
callSiteGetters[i] = {__proto__: null,
// Older Node versions (e.g. v10) don't ship every getter we list
// (isAsync / isPromiseAll / getPromiseIndex landed in Node 12).
// Skip missing entries so applyCallSiteGetters doesn't apply
// `undefined` and throw "Function.prototype.apply was called on undefined".
if (typeof func !== 'function') continue;
callSiteGetters[callSiteGetters.length] = {
__proto__: null,
name,
propName: '_' + name,
func: (thiz) => {
func: thiz => {
return localReflectApply(func, thiz, []);
}
},
};

@@ -447,8 +674,52 @@ }

// SECURITY (GHSA-v27g-jcqj-v8rw): a "host frame" is any frame whose source
// filename indicates host-realm code: an absolute path (starts with `/`),
// a Windows-style absolute path (matches `<letter>:\`), a Node internals
// pseudo-path (starts with `node:` or `internal/`), or a relative path
// containing `..` (host modules sometimes appear with relative paths).
// Clean sandbox filenames (e.g. the default `vm.js`, or user-provided
// VMScript filenames without separators) do NOT match — sandbox
// developers can still see their own line numbers and function names.
function isHostFrameFileName(name) {
if (typeof name !== 'string' || name.length === 0) return false;
if (name.charCodeAt(0) === 0x2F /* '/' */) return true;
if (name.length >= 2 && name.charCodeAt(1) === 0x3A /* ':' */) return true;
if (name.length >= 5 && name.slice(0, 5) === 'node:') return true;
if (name.length >= 9 && name.slice(0, 9) === 'internal/') return true;
return false;
}
function applyCallSiteGetters(thiz, callSite, getters) {
for (let i=0; i<getters.length; i++) {
// SECURITY (GHSA-v27g-jcqj-v8rw): classify the frame once (host vs sandbox)
// by inspecting the underlying CallSite's getFileName. Host frames return
// null for every getter — closes the path/line/function-name leak via
// custom `Error.prepareStackTrace`.
let fileName;
try {
fileName = localReflectApply(OriginalCallSite.prototype.getFileName, callSite, []);
} catch (e) {
fileName = null;
}
const isHostFrame = isHostFrameFileName(fileName);
for (let i = 0; i < getters.length; i++) {
const getter = getters[i];
let value;
if (isHostFrame) {
value = null;
} else if (getter.name === 'getEvalOrigin') {
// SECURITY (post-GHSA-v27g hardening): a sandbox frame's
// `getEvalOrigin()` returns a string of the form
// `"eval at FUNC (FILENAME:LINE:COL)"` whose embedded
// FILENAME may be a host-realm path (e.g. eval triggered
// from `lib/setup-sandbox.js`). The frame-level host
// classifier above does not inspect that nested path.
// Sandbox developers don't need eval-origin info for
// debugging their own code, so always redact.
value = null;
} else {
value = getter.func(callSite);
}
localReflectDefineProperty(thiz, getter.propName, {
__proto__: null,
value: getter.func(callSite)
value,
});

@@ -472,3 +743,3 @@ }

'isPromiseAll',
'getPromiseIndex'
'getPromiseIndex',
]);

@@ -491,4 +762,3 @@

for (let i=0; i<callSiteGetters.length; i++) {
for (let i = 0; i < callSiteGetters.length; i++) {
const name = callSiteGetters[i].name;

@@ -498,5 +768,7 @@ const funcProp = localReflectGetOwnPropertyDescriptor(OriginalCallSite.prototype, name);

const propertyName = callSiteGetters[i].propName;
const func = {func() {
return this[propertyName];
}}.func;
const func = {
func() {
return this[propertyName];
},
}.func;
const nameProp = localReflectGetOwnPropertyDescriptor(func, 'name');

@@ -510,51 +782,75 @@ if (!nameProp) throw localUnexpected();

if (!localReflectDefineProperty(LocalError, 'prepareStackTrace', {
configurable: false,
enumerable: false,
get() {
return currentPrepareStackTrace;
},
set(value) {
if (typeof(value) !== 'function') {
currentPrepareStackTrace = value;
return;
}
const wrapped = localReflectApply(localWeakMapGet, wrappedPrepareStackTrace, [value]);
if (wrapped) {
currentPrepareStackTrace = wrapped;
return;
}
const newWrapped = (error, sst) => {
const sandboxSst = ensureThis(sst);
if (localArrayIsArray(sst)) {
if (sst === sandboxSst) {
for (let i=0; i < sst.length; i++) {
const cs = sst[i];
if (typeof cs === 'object' && localReflectGetPrototypeOf(cs) === OriginalCallSite.prototype) {
sst[i] = new CallSite(cs);
if (
!localReflectDefineProperty(LocalError, 'prepareStackTrace', {
configurable: false,
enumerable: false,
get() {
return currentPrepareStackTrace;
},
set(value) {
// HARDENING (post-#563): when user sets prepareStackTrace to a
// non-function (undefined / null / etc.), substitute the safe
// default so V8 never falls back to Node's host-side formatter
// (which throws host-realm TypeError on Symbol-named errors).
// Crucially, route the default through the SAME wrapping path
// as user-provided functions below — that wraps each CallSite
// in the sandbox-realm `CallSite` class so `' at ' + cs`
// uses our wrapper's safe `toString()` ('CallSite {}') instead
// of V8's native CallSite toString (which leaks absolute host
// paths and host function names into the formatted string).
if (typeof value !== 'function') {
value = defaultSandboxPrepareStackTrace;
}
const wrapped = localReflectApply(localWeakMapGet, wrappedPrepareStackTrace, [value]);
if (wrapped) {
currentPrepareStackTrace = wrapped;
return;
}
const newWrapped = (error, sst) => {
const sandboxSst = ensureThis(sst);
if (localArrayIsArray(sst)) {
if (sst === sandboxSst) {
for (let i = 0; i < sst.length; i++) {
const cs = sst[i];
if (
typeof cs === 'object' &&
localReflectGetPrototypeOf(cs) === OriginalCallSite.prototype
) {
sst[i] = new CallSite(cs);
}
}
} else {
sst = [];
for (let i = 0; i < sandboxSst.length; i++) {
const cs = sandboxSst[i];
localReflectDefineProperty(sst, i, {
__proto__: null,
value: new CallSite(cs),
enumerable: true,
configurable: true,
writable: true,
});
}
}
} else {
sst = [];
for (let i=0; i < sandboxSst.length; i++) {
const cs = sandboxSst[i];
localReflectDefineProperty(sst, i, {
__proto__: null,
value: new CallSite(cs),
enumerable: true,
configurable: true,
writable: true
});
}
sst = sandboxSst;
}
} else {
sst = sandboxSst;
}
return value(error, sst);
};
localReflectApply(localWeakMapSet, wrappedPrepareStackTrace, [value, newWrapped]);
localReflectApply(localWeakMapSet, wrappedPrepareStackTrace, [newWrapped, newWrapped]);
currentPrepareStackTrace = newWrapped;
}
})) throw localUnexpected();
return value(error, sst);
};
localReflectApply(localWeakMapSet, wrappedPrepareStackTrace, [value, newWrapped]);
localReflectApply(localWeakMapSet, wrappedPrepareStackTrace, [newWrapped, newWrapped]);
currentPrepareStackTrace = newWrapped;
},
})
)
throw localUnexpected();
// SECURITY (post-GHSA-v27g Path A residual): assign the safe default
// through the setter so `currentPrepareStackTrace` is the wrapped
// default (not `undefined`). Without this, V8 falls back to Node's
// host-side `defaultPrepareStackTrace` until sandbox code first
// assigns to `Error.prepareStackTrace` — emitting absolute host paths
// in `error.stack` and throwing host-realm TypeError on Symbol-named
// errors.
LocalError.prepareStackTrace = defaultSandboxPrepareStackTrace;
} else if (oldPrepareStackTraceDesc) {

@@ -571,3 +867,3 @@ localReflectDefineProperty(LocalError, 'prepareStackTrace', oldPrepareStackTraceDesc);

/*
* SuppressedError sanitization
* SuppressedError / AggregateError sanitization
*

@@ -580,32 +876,68 @@ * When V8 internally creates SuppressedError during DisposableStack.dispose()

*
* Fix: handleException detects SuppressedError instances and recursively
* sanitizes their .error and .suppressed properties via ensureThis.
* The same sub-error-sanitization gap applies to AggregateError, which
* Promise.any produces when every contributing promise rejects. If any
* contributing promise was host-realm (GHSA-55hx-c926-fr95 / -35vh-489p-v7cx
* class — host-Promise rejection delivery), its rejection value ends up as
* an element of AggregateError.errors[] and reaches sandbox code unsanitized.
*
* Fix: handleException detects SuppressedError / AggregateError instances
* and recursively sanitizes .error / .suppressed / .errors[] via ensureThis.
*/
const localSuppressedErrorProto = (typeof SuppressedError === 'function') ? SuppressedError.prototype : null;
const localSuppressedErrorProto = typeof SuppressedError === 'function' ? SuppressedError.prototype : null;
const localAggregateErrorProto = typeof AggregateError === 'function' ? AggregateError.prototype : null;
function handleException(e, visited) {
e = ensureThis(e);
if (localSuppressedErrorProto !== null && e !== null && typeof e === 'object') {
if (!visited) visited = new LocalWeakMap();
// Cycle detection: if we've already visited this object, stop recursing
if (apply(localWeakMapGet, visited, [e])) return e;
apply(localWeakMapSet, visited, [e, true]);
let proto;
// SECURITY (post-GHSA-mpf8 hardening): use `from` (not `ensureThis`) so
// unmapped null-proto host objects always get a bridge proxy. ensureThis
// returned the raw `other` for null-proto values, leaving an asymmetry
// vs the fulfillment path. `from` is idempotent on already-wrapped
// proxies and pass-through on primitives, so the SuppressedError /
// AggregateError prototype walk below still functions correctly.
e = from(e);
if (e === null || (typeof e !== 'object' && typeof e !== 'function')) return e;
if (localSuppressedErrorProto === null && localAggregateErrorProto === null) return e;
if (!visited) visited = new LocalWeakMap();
// Cycle detection: if we've already visited this object, stop recursing
if (apply(localWeakMapGet, visited, [e])) return e;
apply(localWeakMapSet, visited, [e, true]);
let proto;
try {
proto = localReflectGetPrototypeOf(e);
} catch (ex) {
return e;
}
while (proto !== null) {
if (localSuppressedErrorProto !== null && proto === localSuppressedErrorProto) {
// SECURITY: SuppressedError.error / .suppressed frequently carry
// host-realm errors produced by V8 internals (DisposableStack,
// `using` declarations). Recursively sanitize both branches.
try { e.error = handleException(e.error, visited); } catch (ex) { /* best effort */ }
try { e.suppressed = handleException(e.suppressed, visited); } catch (ex) { /* best effort */ }
return e;
}
if (localAggregateErrorProto !== null && proto === localAggregateErrorProto) {
// SECURITY (GHSA-55hx-c926-fr95): AggregateError.errors[] can carry
// host-realm rejection values when contributing promises in a
// Promise.any call are host-realm. Sanitize each entry.
let arr;
try { arr = e.errors; } catch (ex) { return e; }
if (localArrayIsArray(arr)) {
let len;
try { len = arr.length >>> 0; } catch (ex) { return e; }
for (let i = 0; i < len; i++) {
let item;
try { item = arr[i]; } catch (ex) { continue; }
const sanitized = handleException(item, visited);
if (sanitized !== item) {
try { arr[i] = sanitized; } catch (ex) { /* best effort */ }
}
}
}
return e;
}
try {
proto = localReflectGetPrototypeOf(e);
proto = localReflectGetPrototypeOf(proto);
} catch (ex) {
return e;
}
while (proto !== null) {
if (proto === localSuppressedErrorProto) {
e.error = handleException(e.error, visited);
e.suppressed = handleException(e.suppressed, visited);
return e;
}
try {
proto = localReflectGetPrototypeOf(proto);
} catch (ex) {
return e;
}
}
}

@@ -615,2 +947,22 @@ return e;

// SECURITY (GHSA-55hx): install handleException / ensureThis as the
// sandbox-side sanitizers for host-realm Promise.prototype.then|catch|finally
// invocations. Without this, when sandbox code calls .then/.catch on a host
// Promise (returned e.g. by an embedder-exposed `async () => {}`), the host
// Promise machinery (PromiseReactionJob) runs the sandbox callback against
// the RAW host rejection value, bypassing the sandbox-side Promise.prototype
// override at lines 199-228. The bridge apply-trap interception on those
// methods now wraps callbacks through the same sanitizers, closing the
// invariant: every sandbox callback bound to a Promise — host or sandbox
// realm — receives its argument(s) routed through from() (fulfillment)
// or handleException (rejection, which wraps via from() internally).
//
// SECURITY (post-GHSA-mpf8 hardening): use `from` (not `ensureThis`) for the
// fulfillment sanitiser so unmapped null-proto host values are wrapped in a
// bridge proxy. Mirrors the sandbox-side `globalPromise.prototype.then`
// onFulfilled wrap above.
if (typeof bridge.setHostPromiseSanitizers === 'function') {
bridge.setHostPromiseSanitizers(handleException, from);
}
const withProxy = localObjectFreeze({

@@ -621,3 +973,3 @@ __proto__: null,

return localReflectHas(target, key);
}
},
});

@@ -634,12 +986,15 @@

throw new VMError('Dynamic Import not supported');
}
},
});
if (!localReflectDefineProperty(global, host.INTERNAL_STATE_NAME, {
__proto__: null,
configurable: false,
enumerable: false,
writable: false,
value: interanState
})) throw localUnexpected();
if (
!localReflectDefineProperty(global, host.INTERNAL_STATE_NAME, {
__proto__: null,
configurable: false,
enumerable: false,
writable: false,
value: interanState,
})
)
throw localUnexpected();

@@ -676,3 +1031,3 @@ /*

return makeFunction(args, this.isAsync, this.isGenerator);
}
},
};

@@ -691,3 +1046,3 @@

return localEval(code);
}
},
};

@@ -702,3 +1057,3 @@

throw throwAsync();
}
},
};

@@ -711,3 +1066,3 @@

isAsync,
isGenerator
isGenerator,
};

@@ -718,7 +1073,12 @@ }

const proxy = new LocalProxy(value, handler);
if (!localReflectDefineProperty(obj, prop, {__proto__: null, value: proxy})) throw localUnexpected();
if (!localReflectDefineProperty(obj, prop, { __proto__: null, value: proxy })) throw localUnexpected();
return proxy;
}
const proxiedFunction = overrideWithProxy(localFunction.prototype, 'constructor', localFunction, makeCheckFunction(false, false));
const proxiedFunction = overrideWithProxy(
localFunction.prototype,
'constructor',
localFunction,
makeCheckFunction(false, false),
);
if (GeneratorFunction) {

@@ -734,3 +1094,8 @@ if (!localReflectSetPrototypeOf(GeneratorFunction, proxiedFunction)) throw localUnexpected();

if (!localReflectSetPrototypeOf(AsyncGeneratorFunction, proxiedFunction)) throw localUnexpected();
overrideWithProxy(AsyncGeneratorFunction.prototype, 'constructor', AsyncGeneratorFunction, makeCheckFunction(true, true));
overrideWithProxy(
AsyncGeneratorFunction.prototype,
'constructor',
AsyncGeneratorFunction,
makeCheckFunction(true, true),
);
}

@@ -742,3 +1107,3 @@

const a = [];
for (let i=0; i < sArgs.length; i++) {
for (let i = 0; i < sArgs.length; i++) {
localReflectDefineProperty(a, i, {

@@ -749,3 +1114,3 @@ __proto__: null,

configurable: true,
writable: true
writable: true,
});

@@ -763,3 +1128,3 @@ }

return localReflectConstruct(target, makeSafeHandlerArgs(args), newTarget);
}
},
});

@@ -774,3 +1139,3 @@

return new LocalProxy(value, makeSafeArgs);
}
},
});

@@ -781,3 +1146,3 @@

const handler = args[1];
args[1] = new LocalProxy({__proto__: null, handler}, proxyHandlerHandler);
args[1] = new LocalProxy({ __proto__: null, handler }, proxyHandlerHandler);
return args;

@@ -793,3 +1158,3 @@ }

return localReflectConstruct(target, wrapProxyHandler(args), newTarget);
}
},
});

@@ -810,7 +1175,5 @@

if (localPromise) {
const PromisePrototype = localPromise.prototype;
if (!allowAsync) {
overrideWithProxy(PromisePrototype, 'then', PromisePrototype.then, AsyncErrorHandler);

@@ -821,5 +1184,3 @@ // This seems not to work, and will produce

// Contextify.connect(host.Promise.prototype.then, Promise.prototype.then);
} else {
overrideWithProxy(PromisePrototype, 'then', PromisePrototype.then, {

@@ -847,3 +1208,3 @@ __proto__: null,

return localReflectApply(target, thiz, args);
}
},
});

@@ -864,5 +1225,4 @@

return localReflectApply(target, thiz, args);
}
},
});
}

@@ -942,3 +1302,2 @@

function readonly(other, mock) {

@@ -948,3 +1307,6 @@ // Note: other@other(unsafe) mock@other(unsafe) returns@this(unsafe) throws@this(unsafe)

const tmock = from(mock);
return fromWithFactory(obj=>new ReadOnlyMockHandler(obj, tmock), other);
// SECURITY (GHSA-v37h-5mfm-c47c): use the token-bound helper instead of
// `new ReadOnlyMockHandler(...)`. The handler class is no longer
// directly constructible from sandbox code.
return fromWithFactory(obj => createReadOnlyMockHandler(obj, tmock), other);
}

@@ -955,3 +1317,3 @@

readonly,
global
global,
};

@@ -54,4 +54,36 @@

code = body;
// Note: Keywords are not allows to contain u escapes
if (!/\b(?:catch|import|async)\b/.test(code)) {
// SECURITY (GHSA-wp5r-2gw5-m7q7): the fast-path skip avoids the AST
// walk for code that contains none of the keywords whose presence
// requires instrumentation. Pre-fix the regex tested only `catch`,
// `import`, `async` — but the AST walker also instruments `with`
// statements (wraps the head in `wrapWith()` to enforce the
// `INTERNAL_STATE_NAME` unscopable invariant) and rejects the
// `INTERNAL_STATE_NAME` identifier outright. Code that contained
// neither `catch`/`import`/`async` could therefore reach
// `VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL` and use a
// raw `with()` statement, bypassing both controls. Add `with` to
// the keyword set, and additionally substring-test the entire
// source for `INTERNAL_STATE_NAME` so the identifier check cannot
// be skipped. Keywords cannot contain `u`-escape sequences in
// JavaScript source, so a simple regex over `code` is sufficient.
// Defensive: a custom compiler may return undefined (existing test
// "modules > optionally can run a custom compiler function" exercises
// this). Pre-fix the regex.test below silently coerced undefined to
// "undefined" and returned false; the new substring check would
// throw. Coerce to a defined string for both checks.
//
// SECURITY (post-GHSA-wp5r-2gw5-m7q7 hardening): identifiers in
// JavaScript CAN contain `\uXXXX` / `\u{...}` unicode escapes —
// `VM2_INTERNAL_STATE_DO_NOT_USE_OR_PROGRAM_WILL_FAIL` is a
// valid identifier reference for the same global. The `indexOf`
// substring check above only matches the raw form, so bypassed.
// Force fall-through to AST when the source contains any `\u`
// escape; the AST walker decodes escapes and inspects the actual
// identifier name.
const codeStr = typeof code === 'string' ? code : '';
if (
!/\b(?:catch|import|async|with)\b/.test(codeStr) &&
codeStr.indexOf(INTERNAL_STATE_NAME) === -1 &&
codeStr.indexOf('\\u') === -1
) {
return {__proto__: null, code, hasAsync: false};

@@ -58,0 +90,0 @@ }

+66
-60

@@ -23,29 +23,10 @@ 'use strict';

const pa = require('path');
const {
Script,
createContext
} = require('vm');
const {
EventEmitter
} = require('events');
const {
INSPECT_MAX_BYTES
} = require('buffer');
const {
createBridge,
VMError
} = require('./bridge');
const {
transformer,
INTERNAL_STATE_NAME
} = require('./transformer');
const {
lookupCompiler
} = require('./compiler');
const {
VMScript
} = require('./script');
const {
inspect
} = require('util');
const { Script, createContext } = require('vm');
const { EventEmitter } = require('events');
const { INSPECT_MAX_BYTES } = require('buffer');
const { createBridge, VMError } = require('./bridge');
const { transformer, INTERNAL_STATE_NAME } = require('./transformer');
const { lookupCompiler } = require('./compiler');
const { VMScript } = require('./script');
const { inspect } = require('util');

@@ -65,3 +46,3 @@ const objectDefineProperties = Object.defineProperties;

INSPECT_MAX_BYTES,
INTERNAL_STATE_NAME
INTERNAL_STATE_NAME,
});

@@ -81,3 +62,3 @@

filename,
displayErrors: false
displayErrors: false,
});

@@ -91,3 +72,3 @@ }

*/
const DEFAULT_RUN_OPTIONS = Object.freeze({__proto__: null, displayErrors: false});
const DEFAULT_RUN_OPTIONS = Object.freeze({ __proto__: null, displayErrors: false });

@@ -134,3 +115,3 @@ function checkAsync(allow) {

filename: 'timeout_bridge.js',
displayErrors: false
displayErrors: false,
});

@@ -143,3 +124,3 @@ }

displayErrors: false,
timeout
timeout,
});

@@ -151,6 +132,10 @@ } finally {

const bridgeScript = compileScript(`${__dirname}/bridge.js`,
`(function(global) {"use strict"; const exports = {};${fs.readFileSync(`${__dirname}/bridge.js`, 'utf8')}\nreturn exports;})`);
const setupSandboxScript = compileScript(`${__dirname}/setup-sandbox.js`,
`(function(global, host, bridge, data, context) { ${fs.readFileSync(`${__dirname}/setup-sandbox.js`, 'utf8')}\n})`);
const bridgeScript = compileScript(
`${__dirname}/bridge.js`,
`(function(global) {"use strict"; const exports = {};${fs.readFileSync(`${__dirname}/bridge.js`, 'utf8')}\nreturn exports;})`,
);
const setupSandboxScript = compileScript(
`${__dirname}/setup-sandbox.js`,
`(function(global, host, bridge, data, context) { ${fs.readFileSync(`${__dirname}/setup-sandbox.js`, 'utf8')}\n})`,
);
const getGlobalScript = compileScript('get_global.js', 'this');

@@ -168,3 +153,6 @@

try {
getAsyncGeneratorFunctionScript = compileScript('get_async_generator_function.js', '(async function*(){}).constructor');
getAsyncGeneratorFunctionScript = compileScript(
'get_async_generator_function.js',
'(async function*(){}).constructor',
);
} catch (ex) {}

@@ -178,3 +166,2 @@

class VM extends EventEmitter {
/**

@@ -231,2 +218,7 @@ * The timeout for {@link VM#run} calls.

* @param {boolean} [options.allowAsync=true] - Allows for async functions.
* @param {number} [options.bufferAllocLimit=Infinity] - Maximum size in bytes for a single
* Buffer.alloc / allocUnsafe / allocUnsafeSlow / Buffer(N) / new Buffer(N) call. Caps a
* single-shot synchronous host allocation primitive that V8's `timeout` cannot interrupt
* (see GHSA-6785-pvv7-mvg7). Defaults to Infinity (no cap); set to a finite byte count
* (e.g. 32 * 1024 * 1024) when sandboxing untrusted code as part of layered DoS defense.
* @throws {VMError} If the compiler is unknown.

@@ -243,3 +235,4 @@ */

compilerOptions,
allowAsync: optAllowAsync = true
allowAsync: optAllowAsync = true,
bufferAllocLimit = Infinity,
} = options;

@@ -250,2 +243,8 @@ const allowEval = options.eval !== false;

// SECURITY (GHSA-6785-pvv7-mvg7): validate bufferAllocLimit. Reject negatives/NaN.
// Allow Infinity for callers who explicitly opt out of the cap.
if (typeof bufferAllocLimit !== 'number' || bufferAllocLimit < 0 || Number.isNaN(bufferAllocLimit)) {
throw new VMError('bufferAllocLimit must be a non-negative number.');
}
// Early error if sandbox is not an object.

@@ -265,4 +264,4 @@ if (sandbox && 'object' !== typeof sandbox) {

strings: allowEval,
wasm: allowWasm
}
wasm: allowWasm,
},
});

@@ -273,5 +272,6 @@

// Initialize the sandbox bridge
const {
createBridge: sandboxCreateBridge
} = bridgeScript.runInContext(_context, DEFAULT_RUN_OPTIONS)(sandboxGlobal);
const { createBridge: sandboxCreateBridge } = bridgeScript.runInContext(
_context,
DEFAULT_RUN_OPTIONS,
)(sandboxGlobal);

@@ -283,3 +283,4 @@ // Initialize the bridge

__proto__: null,
allowAsync
allowAsync,
bufferAllocLimit,
};

@@ -298,5 +299,11 @@

// Create the bridge between the host and the sandbox.
const internal = setupSandboxScript.runInContext(_context, DEFAULT_RUN_OPTIONS)(sandboxGlobal, HOST, bridge.other, data, _context);
const internal = setupSandboxScript.runInContext(_context, DEFAULT_RUN_OPTIONS)(
sandboxGlobal,
HOST,
bridge.other,
data,
_context,
);
const runScript = (script) => {
const runScript = script => {
// This closure is intentional to hide _context and bridge since the allow to access the sandbox directly which is unsafe.

@@ -321,3 +328,3 @@ let ret;

const makeProtected = (value) => {
const makeProtected = value => {
const sandboxBridge = bridge.other;

@@ -368,3 +375,3 @@ try {

writable: true,
enumerable: true
enumerable: true,
},

@@ -374,3 +381,3 @@ compiler: {

value: compiler,
enumerable: true
enumerable: true,
},

@@ -380,11 +387,11 @@ sandbox: {

value: bridge.from(sandboxGlobal),
enumerable: true
enumerable: true,
},
_runScript: {__proto__: null, value: runScript},
_makeReadonly: {__proto__: null, value: makeReadonly},
_makeProtected: {__proto__: null, value: makeProtected},
_addProtoMapping: {__proto__: null, value: addProtoMapping},
_addProtoMappingFactory: {__proto__: null, value: addProtoMappingFactory},
_compiler: {__proto__: null, value: resolvedCompiler},
_allowAsync: {__proto__: null, value: allowAsync}
_runScript: { __proto__: null, value: runScript },
_makeReadonly: { __proto__: null, value: makeReadonly },
_makeProtected: { __proto__: null, value: makeProtected },
_addProtoMapping: { __proto__: null, value: addProtoMapping },
_addProtoMappingFactory: { __proto__: null, value: addProtoMappingFactory },
_compiler: { __proto__: null, value: resolvedCompiler },
_allowAsync: { __proto__: null, value: allowAsync },
});

@@ -524,3 +531,3 @@

filename: useFileName,
displayErrors: false
displayErrors: false,
});

@@ -563,5 +570,4 @@ }

}
}
exports.VM = VM;

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

],
"version": "3.10.5",
"version": "3.11.0",
"main": "index.js",

@@ -28,3 +28,3 @@ "sideEffects": false,

"eslint": "^9.38.0",
"mocha": "^11.1.0"
"mocha": "^12.0.0-beta-9.2"
},

@@ -35,3 +35,3 @@ "engines": {

"scripts": {
"test": "mocha test --ignore test/compilers.js",
"test": "mocha test --recursive --ignore test/compilers.js",
"test:compilers": "mocha test/compilers.js",

@@ -38,0 +38,0 @@ "lint": "eslint ."

@@ -145,2 +145,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]

- `allowAsync` - If set to `false` any attempt to run code using `async` will throw a `VMError` (default: `true`).
- `bufferAllocLimit` - Maximum size in bytes for a single `Buffer.alloc` / `Buffer.allocUnsafe` / `Buffer.allocUnsafeSlow` / `Buffer(N)` / `new Buffer(N)` request from inside the sandbox. Requests that exceed this cap throw a `RangeError` synchronously without performing the host allocation. Default: `Infinity` (no cap, fully backwards-compatible). Embedders running untrusted code in memory-constrained environments (Docker / Kubernetes / Lambda / serverless) should opt into a finite cap (e.g. `32 * 1024 * 1024`) as part of layered DoS defense, the same way they opt into `timeout`. See [Hardening recommendations](#hardening-recommendations) below.

@@ -180,2 +181,3 @@ **IMPORTANT**: Timeout is only effective on synchronous code that you run through `run`. Timeout does **NOT** work on any method returned by VM. There are some situations when timeout doesn't work - see [#244](https://github.com/patriksimek/vm2/pull/244).

- `wasm` - If set to `false` any attempt to compile a WebAssembly module will throw a `WebAssembly.CompileError` (default: `true`). Note: `WebAssembly.JSTag` is removed inside the sandbox for security reasons, so wasm code cannot catch JavaScript exceptions.
- `bufferAllocLimit` - Same semantics as on `VM` — maximum size in bytes for a single `Buffer.alloc` family request from inside the sandbox. Default: `Infinity`. See [Hardening recommendations](#hardening-recommendations).
- `sourceExtensions` - Array of file extensions to treat as source code (default: `['js']`).

@@ -448,2 +450,59 @@ - `require` - `true`, an object or a Resolver to enable `require` method (default: `false`).

## Hardening recommendations
vm2 prevents sandbox escapes (untrusted code obtaining host realm access). It does **not**, by itself, prevent every form of resource exhaustion or denial-of-service. Embedders running untrusted code should add the following layered defenses around the sandbox.
### 1. Cap memory allocation with `bufferAllocLimit`
A single `Buffer.alloc(N)` call with attacker-controlled `N` runs as one synchronous host C++ allocation that V8's `timeout` cannot interrupt. In memory-constrained environments a ~100-byte sandbox payload can drive a 100 MB+ host RSS jump and crash the host process via OOM. Set `bufferAllocLimit` (e.g. `32 * 1024 * 1024`) to cap individual allocations:
```js
const vm = new VM({
timeout: 1000,
bufferAllocLimit: 32 * 1024 * 1024,
allowAsync: false,
});
```
The cap also applies to the deprecated `Buffer(N)` and `new Buffer(N)` paths. Note that aggregate exhaustion (many small allocations, `Buffer.concat`, `Uint8Array`, `String.repeat`, `Array(n).fill()`, etc.) is **not** covered by this cap — combine with a host-side memory limit (`--max-old-space-size`, container limit, cgroup) for full coverage.
### 2. Install a host-side `unhandledRejection` handler
A class of host-process abort DoS exists where sandbox code creates an `async function`, `async function*`, or `await using` whose body throws a value that triggers a host-realm error during stack formatting (e.g. `e.name = Symbol(); e.stack`). V8 creates the rejection promise via the realm's intrinsic Promise, which bypasses vm2's `Promise` subclass wrap, so the rejection escapes to the host as `unhandledRejection`. On Node 15+ the default behavior is to terminate the process.
Closing this requires changing observable host behavior, so vm2 does not ship a fix by default. Embedders should install a process-level handler that swallows (or logs) sandbox-originating rejections:
```js
// Recommended: filter rejections that originated inside vm2 and swallow them,
// while letting your own host-side rejections propagate.
process.on('unhandledRejection', (reason, promise) => {
// Heuristic: rejections from the sandbox frequently surface as values
// without proper Error semantics, or with stacks pointing at vm.js.
// Adjust the predicate to match your application.
if (looksLikeSandboxOrigin(reason)) {
return; // swallow — don't terminate the process
}
// Otherwise: handle (or rethrow) as normal for your host code.
yourLogger.error('unhandled rejection', reason);
});
```
If your application has no other source of unhandled rejections, a blanket swallow + log is acceptable:
```js
process.on('unhandledRejection', reason => {
yourLogger.warn('swallowed sandbox rejection', reason);
});
```
A scoped fix may ship behind an opt-in `swallowSandboxUnhandledRejections` flag in a future minor release; until then, the host-side handler is the recommended mitigation.
### 3. Run with a process-level memory cap
Even with `bufferAllocLimit` set, run the host process with `--max-old-space-size` (or an equivalent container memory limit) sized for the workload. The cap protects against the single-allocation primitive; the OS-level limit protects against aggregate exhaustion and against any future allocation primitive vm2 hasn't yet capped.
### 4. Treat `require.builtin: ['*']` as a non-sandbox configuration
The `'*'` wildcard expands to most Node built-ins, including `child_process`, `fs`, `dgram`, `net`, `http`, and `dns`. These are full host-capability primitives — `require('child_process').execSync('id')` is reachable from the sandbox under `'*'`. vm2's `'*'` semantics are intentional (some embedders run trusted-but-isolated code), but it should not be used as a default for untrusted code. Prefer an explicit allowlist of the smallest set of modules your sandbox actually needs.
## Known Issues

@@ -455,3 +514,3 @@

- Source code transformations can result a different source string for a function.
- There are ways to crash the node process from inside the sandbox.
- There are ways to crash the node process from inside the sandbox. See [Hardening recommendations](#hardening-recommendations).

@@ -458,0 +517,0 @@ [npm-image]: https://img.shields.io/npm/v/vm2.svg

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