🚀 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.4
to
3.10.5
+252
-71
lib/bridge.js

@@ -43,2 +43,3 @@ 'use strict';

'URIError',
'SuppressedError',
'Error'

@@ -67,2 +68,49 @@ ];

// Add non-global function constructor prototypes for cross-realm blocking.
// These are not on `global` but are needed to block sandbox escape via
// AsyncFunction('code'), GeneratorFunction('code'), etc.
try {
thisGlobalPrototypes['AsyncFunction'] = (async function() {}).constructor.prototype;
} catch (e) {}
try {
thisGlobalPrototypes['GeneratorFunction'] = (function*() {}).constructor.prototype;
} catch (e) {}
try {
// Use eval to avoid syntax error on Node < 10 where async generators don't exist
thisGlobalPrototypes['AsyncGeneratorFunction'] = eval('(async function*() {})').constructor.prototype;
} catch (e) {}
// Cache this-realm dangerous function constructors.
// Used to block raw host Function constructors from leaking when handler
// methods are called directly (e.g., via showProxy handler exposure).
// This complements isDangerousFunctionConstructor which checks OTHER-realm constructors.
let thisAsyncFunctionCtor;
let thisGeneratorFunctionCtor;
let thisAsyncGeneratorFunctionCtor;
try {
if (thisGlobalPrototypes.AsyncFunction) {
const desc = thisReflectGetOwnPropertyDescriptor(thisGlobalPrototypes.AsyncFunction, 'constructor');
if (desc) thisAsyncFunctionCtor = desc.value;
}
} catch (e) {}
try {
if (thisGlobalPrototypes.GeneratorFunction) {
const desc = thisReflectGetOwnPropertyDescriptor(thisGlobalPrototypes.GeneratorFunction, 'constructor');
if (desc) thisGeneratorFunctionCtor = desc.value;
}
} catch (e) {}
try {
if (thisGlobalPrototypes.AsyncGeneratorFunction) {
const desc = thisReflectGetOwnPropertyDescriptor(thisGlobalPrototypes.AsyncGeneratorFunction, 'constructor');
if (desc) thisAsyncGeneratorFunctionCtor = desc.value;
}
} catch (e) {}
function isThisDangerousFunctionConstructor(value) {
return value === thisFunction ||
(thisAsyncFunctionCtor && value === thisAsyncFunctionCtor) ||
(thisGeneratorFunctionCtor && value === thisGeneratorFunctionCtor) ||
(thisAsyncGeneratorFunctionCtor && value === thisAsyncGeneratorFunctionCtor);
}
const {

@@ -240,2 +288,6 @@ getPrototypeOf: thisReflectGetPrototypeOf,

const handlerToObject = new ThisWeakMap();
// Store factory functions in a WeakMap keyed by handler instance.
// This prevents exposure of factory functions via util.inspect with showProxy:true,
// which would allow attackers to create new handlers wrapping attacker-controlled objects.
const handlerToFactory = new ThisWeakMap();

@@ -249,2 +301,9 @@ // Closure-scoped function to retrieve the wrapped object from a handler.

// Closure-scoped function to retrieve the factory from a handler.
// This is NOT a method on BaseHandler, so it cannot be called by attackers
// even if they obtain a reference to the handler via showProxy.
function getHandlerFactory(handler) {
return thisReflectApply(thisWeakMapGet, handlerToFactory, [handler]);
}
// Closure-scoped function to convert other-realm objects to this-realm with factory.

@@ -254,5 +313,60 @@ // This is NOT a method on BaseHandler, so it cannot be called by attackers

function handlerFromOtherWithContext(handler, other) {
return thisFromOtherWithFactory(handler.getFactory(), other);
return thisFromOtherWithFactory(getHandlerFactory(handler), other);
}
// Closure-scoped function to prevent extensions on a proxy target.
// This is NOT a method on BaseHandler, so it cannot be called by attackers
// even if they obtain a reference to the handler via showProxy.
// Unlike other handler methods which use getHandlerObject(this), the old
// doPreventExtensions accepted `object` as a direct parameter, allowing
// attackers to pass crafted objects. Now it retrieves `object` from the WeakMap.
function doPreventExtensions(handler, target) {
// Note: handler@this(unsafe) target@this(unsafe) throws@this(unsafe)
const object = getHandlerObject(handler); // @other(unsafe)
let keys; // @other(safe-array-of-prim)
try {
keys = otherReflectOwnKeys(object);
} catch (e) { // @other(unsafe)
throw thisFromOtherForThrow(e);
}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]; // @prim
// Skip dangerous cross-realm symbols
if (isDangerousCrossRealmSymbol(key)) continue;
let desc;
try {
desc = otherSafeGetOwnPropertyDescriptor(object, key);
} catch (e) { // @other(unsafe)
throw thisFromOtherForThrow(e);
}
if (!desc) continue;
if (!desc.configurable) {
const current = thisSafeGetOwnPropertyDescriptor(target, key);
if (current && !current.configurable) continue;
if (desc.get || desc.set) {
desc.get = handlerFromOtherWithContext(handler, desc.get);
desc.set = handlerFromOtherWithContext(handler, desc.set);
} else if (typeof object === 'function' && (key === 'caller' || key === 'callee' || key === 'arguments')) {
desc.value = null;
} else {
desc.value = handlerFromOtherWithContext(handler, desc.value);
}
} else {
if (desc.get || desc.set) {
desc = {
__proto__: null,
configurable: true,
enumerable: desc.enumerable,
writable: true,
value: null
};
} else {
desc.value = null;
}
}
if (!thisReflectDefineProperty(target, key, desc)) throw thisUnexpected();
}
if (!thisReflectPreventExtensions(target)) throw thisUnexpected();
}
function thisAddProtoMapping(proto, other, name) {

@@ -262,3 +376,3 @@ // Note: proto@this(unsafe) other@other(unsafe) name@this(unsafe) throws@this(unsafe)

thisReflectApply(thisMapSet, protoMappings, [other,
(factory, object) => thisProxyOther(factory, object, proto)]);
(factory, object, preventUnwrap) => thisProxyOther(factory, object, proto, preventUnwrap)]);
if (name) thisReflectApply(thisMapSet, protoName, [proto, name]);

@@ -271,3 +385,3 @@ }

thisReflectApply(thisMapSet, protoMappings, [other,
(factory, object) => {
(factory, object, preventUnwrap) => {
if (!proto) {

@@ -278,3 +392,3 @@ proto = protoFactory();

}
return thisProxyOther(factory, object, proto);
return thisProxyOther(factory, object, proto, preventUnwrap);
}]);

@@ -345,2 +459,84 @@ }

// Cache the other realm's Function constructors to block them from crossing the bridge.
// This prevents sandbox escape via indirect access paths like
// Object.getOwnPropertyDescriptor(Function.prototype, 'constructor').value
// We block all code-executing constructors: Function, AsyncFunction, GeneratorFunction, AsyncGeneratorFunction
// IMPORTANT: We must get these from otherGlobalPrototypes (the OTHER realm), not from
// local function instances which would give us THIS realm's constructors.
let otherFunctionCtor;
let otherAsyncFunctionCtor;
let otherGeneratorFunctionCtor;
let otherAsyncGeneratorFunctionCtor;
try {
const desc = otherSafeGetOwnPropertyDescriptor(otherGlobalPrototypes.Function, 'constructor');
if (desc) otherFunctionCtor = desc.value;
} catch (e) {
// If we can't get it, the get trap's constructor case still provides protection
}
// Get AsyncFunction, GeneratorFunction, AsyncGeneratorFunction constructors from OTHER realm
try {
if (otherGlobalPrototypes.AsyncFunction) {
const desc = otherSafeGetOwnPropertyDescriptor(otherGlobalPrototypes.AsyncFunction, 'constructor');
if (desc) otherAsyncFunctionCtor = desc.value;
}
} catch (e) {}
try {
if (otherGlobalPrototypes.GeneratorFunction) {
const desc = otherSafeGetOwnPropertyDescriptor(otherGlobalPrototypes.GeneratorFunction, 'constructor');
if (desc) otherGeneratorFunctionCtor = desc.value;
}
} catch (e) {}
try {
if (otherGlobalPrototypes.AsyncGeneratorFunction) {
const desc = otherSafeGetOwnPropertyDescriptor(otherGlobalPrototypes.AsyncGeneratorFunction, 'constructor');
if (desc) otherAsyncGeneratorFunctionCtor = desc.value;
}
} catch (e) {}
function isDangerousFunctionConstructor(value) {
return value === otherFunctionCtor ||
value === otherAsyncFunctionCtor ||
value === otherGeneratorFunctionCtor ||
(otherAsyncGeneratorFunctionCtor && value === otherAsyncGeneratorFunctionCtor); // AsyncGeneratorFunction is not available on Node < 10
}
// Check if an object contains a dangerous function constructor in ANY of its
// own property values (data or accessor), recursively at any nesting depth.
// Uses cycle detection and otherSafeGetOwnPropertyDescriptor (not otherReflectGet)
// to avoid triggering getters.
function containsDangerousConstructor(obj, visited) {
if (obj === null || typeof obj !== 'object') return false;
if (!visited) visited = new ThisWeakMap();
if (thisReflectApply(thisWeakMapGet, visited, [obj])) return false;
thisReflectApply(thisWeakMapSet, visited, [obj, true]);
let keys;
try {
keys = otherReflectOwnKeys(obj);
} catch (e) {
return false;
}
for (let i = 0; i < keys.length; i++) {
let desc;
try {
desc = otherSafeGetOwnPropertyDescriptor(obj, keys[i]);
} catch (e) {
continue;
}
if (!desc) continue;
if (desc.get || desc.set) {
if (isDangerousFunctionConstructor(desc.get) || isDangerousFunctionConstructor(desc.set)) return true;
} else {
if (isDangerousFunctionConstructor(desc.value)) return true;
if (desc.value !== null && typeof desc.value === 'object') {
if (containsDangerousConstructor(desc.value, visited)) return true;
}
}
}
return false;
}
function thisOtherHasOwnProperty(object, key) {

@@ -395,56 +591,10 @@ // Note: object@other(safe) key@prim throws@this(unsafe)

thisReflectApply(thisWeakMapSet, handlerToObject, [this, object]);
// Store the factory in a WeakMap instead of as a method.
// NOTE: There is intentionally NO getFactory() method on this class.
// The factory is retrieved via the closure-scoped getHandlerFactory() function,
// which is not accessible to attackers even if they obtain a handler reference.
// Subclass constructors override this with their specific factory.
thisReflectApply(thisWeakMapSet, handlerToFactory, [this, defaultFactory]);
}
getFactory() {
return defaultFactory;
}
doPreventExtensions(target, object, factory) {
// Note: target@this(unsafe) object@other(unsafe) throws@this(unsafe)
let keys; // @other(safe-array-of-prim)
try {
keys = otherReflectOwnKeys(object);
} catch (e) { // @other(unsafe)
throw thisFromOtherForThrow(e);
}
for (let i = 0; i < keys.length; i++) {
const key = keys[i]; // @prim
// Skip dangerous cross-realm symbols
if (isDangerousCrossRealmSymbol(key)) continue;
let desc;
try {
desc = otherSafeGetOwnPropertyDescriptor(object, key);
} catch (e) { // @other(unsafe)
throw thisFromOtherForThrow(e);
}
if (!desc) continue;
if (!desc.configurable) {
const current = thisSafeGetOwnPropertyDescriptor(target, key);
if (current && !current.configurable) continue;
if (desc.get || desc.set) {
desc.get = handlerFromOtherWithContext(this,desc.get);
desc.set = handlerFromOtherWithContext(this,desc.set);
} else if (typeof object === 'function' && (key === 'caller' || key === 'callee' || key === 'arguments')) {
desc.value = null;
} else {
desc.value = handlerFromOtherWithContext(this,desc.value);
}
} else {
if (desc.get || desc.set) {
desc = {
__proto__: null,
configurable: true,
enumerable: desc.enumerable,
writable: true,
value: null
};
} else {
desc.value = null;
}
}
if (!thisReflectDefineProperty(target, key, desc)) throw thisUnexpected();
}
if (!thisReflectPreventExtensions(target)) throw thisUnexpected();
}
get(target, key, receiver) {

@@ -458,7 +608,15 @@ if (key === 'isProxy') return true;

if (desc) {
if (desc.value && desc.value.name === 'Function') return {};
if (desc.value && isDangerousFunctionConstructor(desc.value)) return {};
return thisDefaultGet(this, object, key, desc);
}
const proto = thisReflectGetPrototypeOf(target);
return proto === null ? undefined : proto.constructor;
if (proto === null) return undefined;
const ctor = proto.constructor;
// Defense in depth: block this-realm dangerous function constructors.
// Normally handler methods are only called by the proxy mechanism
// which handles return values safely, but if the handler is exposed
// (e.g., via util.inspect showProxy), attackers can call get()
// directly with a forged target, leaking raw host constructors.
if (isThisDangerousFunctionConstructor(ctor)) return {};
return ctor;
}

@@ -542,3 +700,3 @@ case '__proto__': {

}
return thisFromOtherWithFactory(this.getFactory(), ret, thisFromOther(object));
return thisFromOtherWithFactory(getHandlerFactory(this), ret, thisFromOther(object));
}

@@ -550,2 +708,8 @@

if (desc && typeof object === 'function' && (prop === 'arguments' || prop === 'caller' || prop === 'callee')) desc.value = null;
// Block sandbox access to host's Function constructor via getOwnPropertyDescriptor.
// This mirrors the protection in the get() trap at the 'constructor' case.
// Only block when sandbox (!isHost) accesses host objects, not when host inspects sandbox.
if (!isHost && desc && prop === 'constructor' && desc.value && isDangerousFunctionConstructor(desc.value)) {
return undefined;
}
return desc;

@@ -686,3 +850,3 @@ }

if (thisReflectIsExtensible(target)) {
this.doPreventExtensions(target, object, this);
doPreventExtensions(this, target);
}

@@ -728,3 +892,3 @@ return false;

if (thisReflectIsExtensible(target)) {
this.doPreventExtensions(target, object, this);
doPreventExtensions(this, target);
}

@@ -759,4 +923,5 @@ return true;

getFactory() {
return protectedFactory;
constructor(object) {
super(object);
thisReflectApply(thisWeakMapSet, handlerToFactory, [this, protectedFactory]);
}

@@ -793,4 +958,5 @@

getFactory() {
return readonlyFactory;
constructor(object) {
super(object);
thisReflectApply(thisWeakMapSet, handlerToFactory, [this, readonlyFactory]);
}

@@ -867,3 +1033,3 @@

function thisProxyOther(factory, other, proto) {
function thisProxyOther(factory, other, proto, preventUnwrap) {
const target = thisCreateTargetObject(other, proto);

@@ -873,3 +1039,5 @@ const handler = factory(other);

try {
otherReflectApply(otherWeakMapSet, mappingThisToOther, [proxy, other]);
if (!preventUnwrap) {
otherReflectApply(otherWeakMapSet, mappingThisToOther, [proxy, other]);
}
registerProxy(proxy, handler);

@@ -988,6 +1156,19 @@ } catch (e) {

case 'function':
// Block the other realm's Function constructors from crossing the bridge.
if (!isHost && isDangerousFunctionConstructor(other)) {
return emptyFrozenObject;
}
// Cache check first — if already proxied, return existing proxy.
// Safe because: cached proxies were created under the same preventUnwrap
// rules, and an attacker can't retroactively add Function constructors
// to a host object's properties from the sandbox (chicken-and-egg).
const mapped = thisReflectApply(thisWeakMapGet, mappingOtherToThis, [other]);
if (mapped) return mapped;
// For objects on sandbox side, check for nested dangerous constructors.
// If found, proxy WITHOUT unwrap registration (mappingThisToOther), so
// the proxy cannot be unwrapped when passed back to host functions.
// The proxy's get trap already sanitizes dangerous values on read.
const dangerous = !isHost && containsDangerousConstructor(other);
if (proto) {
return thisProxyOther(factory, other, proto);
return thisProxyOther(factory, other, proto, dangerous);
}

@@ -1000,7 +1181,7 @@ try {

if (!proto) {
return thisProxyOther(factory, other, null);
return thisProxyOther(factory, other, null, dangerous);
}
do {
const mapping = thisReflectApply(thisMapGet, protoMappings, [proto]);
if (mapping) return mapping(factory, other);
if (mapping) return mapping(factory, other, dangerous);
try {

@@ -1012,3 +1193,3 @@ proto = otherReflectGetPrototypeOf(proto);

} while (proto);
return thisProxyOther(factory, other, thisObjectPrototype);
return thisProxyOther(factory, other, thisObjectPrototype, dangerous);
case 'undefined':

@@ -1015,0 +1196,0 @@ case 'string':

@@ -208,3 +208,3 @@ /* global host, bridge, data, context */

onRejected = function onRejected(error) {
error = ensureThis(error);
error = handleException(error);
return apply(origOnRejected, this, [error]);

@@ -221,3 +221,3 @@ };

onRejected = function onRejected(error) {
error = ensureThis(error);
error = handleException(error);
return apply(origOnRejected, this, [error]);

@@ -279,2 +279,22 @@ };

/*
* WebAssembly.JSTag protection
*
* WebAssembly.JSTag (Node 25+) allows wasm exception handling to catch JavaScript
* exceptions via try_table/catch with JSTag. This completely bypasses the transformer's
* catch block instrumentation, which only wraps JavaScript catch clauses with
* handleException(). An attacker can:
* 1. Create a wasm module that imports JSTag and catches JS exceptions
* 2. Import a function that triggers a host TypeError (e.g., via Symbol() name trick)
* 3. Catch the host error in wasm, returning it as an externref
* 4. Use the raw host error's constructor chain to escape
*
* Fix: Remove WebAssembly.JSTag from the sandbox. Without it, wasm code cannot
* catch JavaScript exceptions — catch_all provides no value access, and catch_all_ref
* requires JSTag for exn.extract. The tag is a V8 internal and cannot be reconstructed.
*/
if (typeof WebAssembly !== 'undefined' && WebAssembly.JSTag !== undefined) {
localReflectDeleteProperty(WebAssembly, 'JSTag');
}
if (!localReflectDefineProperty(global, 'VMError', {

@@ -527,2 +547,45 @@ __proto__: null,

/*
* SuppressedError sanitization
*
* When V8 internally creates SuppressedError during DisposableStack.dispose()
* or 'using' declarations, the .error and .suppressed properties may contain
* host-realm errors (e.g., TypeError from Symbol() name trick). Since the
* SuppressedError is created in the sandbox context, ensureThis returns it
* as-is, leaving its sub-error properties unsanitized.
*
* Fix: handleException detects SuppressedError instances and recursively
* sanitizes their .error and .suppressed properties via ensureThis.
*/
const localSuppressedErrorProto = (typeof SuppressedError === 'function') ? SuppressedError.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;
try {
proto = localReflectGetPrototypeOf(e);
} 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;
}
}
}
return e;
}
const withProxy = localObjectFreeze({

@@ -542,3 +605,3 @@ __proto__: null,

},
handleException: ensureThis,
handleException,
import(what) {

@@ -738,3 +801,3 @@ throw new VMError('Dynamic Import not supported');

args[1] = function sanitizedOnRejected(error) {
error = ensureThis(error);
error = handleException(error);
return localReflectApply(onRejected, this, [error]);

@@ -755,3 +818,3 @@ };

args[0] = function sanitizedOnRejected(error) {
error = ensureThis(error);
error = handleException(error);
return localReflectApply(onRejected, this, [error]);

@@ -839,7 +902,2 @@ };

localObject.defineProperty(localObject, 'setPrototypeOf', {
value: () => {
throw new VMError('Operation not allowed on contextified object.');
}
});

@@ -846,0 +904,0 @@ function readonly(other, mock) {

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

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

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

"dependencies": {
"acorn": "^8.14.1",
"acorn": "^8.15.0",
"acorn-walk": "^8.3.4"

@@ -26,0 +26,0 @@ },

@@ -143,3 +143,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]

- `eval` - If set to `false` any calls to `eval` or function constructors (`Function`, `GeneratorFunction`, etc.) will throw an `EvalError` (default: `true`).
- `wasm` - If set to `false` any attempt to compile a WebAssembly module will throw a `WebAssembly.CompileError` (default: `true`).
- `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.
- `allowAsync` - If set to `false` any attempt to run code using `async` will throw a `VMError` (default: `true`).

@@ -179,3 +179,3 @@

- `eval` - If set to `false` any calls to `eval` or function constructors (`Function`, `GeneratorFunction`, etc.) will throw an `EvalError` (default: `true`).
- `wasm` - If set to `false` any attempt to compile a WebAssembly module will throw a `WebAssembly.CompileError` (default: `true`).
- `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.
- `sourceExtensions` - Array of file extensions to treat as source code (default: `['js']`).

@@ -182,0 +182,0 @@ - `require` - `true`, an object or a Resolver to enable `require` method (default: `false`).