🚀 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.2
to
3.10.3
+27
-1
lib/bridge.js

@@ -107,3 +107,8 @@ 'use strict';

const thisSymbolNodeJSUtilInspectCustom = Symbol.for('nodejs.util.inspect.custom');
const thisSymbolNodeJSRejection = Symbol.for('nodejs.rejection');
function isDangerousCrossRealmSymbol(key) {
return key === thisSymbolNodeJSUtilInspectCustom || key === thisSymbolNodeJSRejection;
}
/**

@@ -384,2 +389,4 @@ * VMError.

const key = keys[i]; // @prim
// Skip dangerous cross-realm symbols
if (isDangerousCrossRealmSymbol(key)) continue;
let desc;

@@ -524,2 +531,4 @@ try {

// Note: target@this(unsafe) prop@prim throws@this(unsafe)
// Filter dangerous cross-realm symbols to prevent extraction
if (isDangerousCrossRealmSymbol(prop)) return undefined;
const object = this.getObject(); // @other(unsafe)

@@ -634,2 +643,4 @@ let desc; // @other(safe)

// Note: target@this(unsafe) key@prim throws@this(unsafe)
// Filter dangerous cross-realm symbols
if (isDangerousCrossRealmSymbol(key)) return false;
const object = this.getObject(); // @other(unsafe)

@@ -666,3 +677,18 @@ try {

}
return thisFromOther(res);
// Filter dangerous cross-realm symbols to prevent extraction via spread operator
const keys = thisFromOther(res);
const filtered = [];
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!isDangerousCrossRealmSymbol(key)) {
thisReflectDefineProperty(filtered, filtered.length, {
__proto__: null,
value: key,
writable: true,
enumerable: true,
configurable: true
});
}
}
return filtered;
}

@@ -669,0 +695,0 @@

@@ -28,5 +28,10 @@ /* global host, bridge, data, context */

setPrototypeOf: localReflectSetPrototypeOf,
getOwnPropertyDescriptor: localReflectGetOwnPropertyDescriptor
getOwnPropertyDescriptor: localReflectGetOwnPropertyDescriptor,
ownKeys: localReflectOwnKeys
} = localReflect;
const localObjectGetOwnPropertySymbols = localObject.getOwnPropertySymbols;
const localObjectGetOwnPropertyDescriptors = localObject.getOwnPropertyDescriptors;
const localObjectAssign = localObject.assign;
const speciesSymbol = Symbol.species;

@@ -36,5 +41,137 @@ const globalPromise = global.Promise;

/*
* Symbol.for protection
*
* Certain Node.js cross-realm symbols can be exploited for sandbox escapes:
*
* - 'nodejs.util.inspect.custom': Called by util.inspect with host's inspect function as argument.
* If sandbox defines this on an object passed to host APIs (e.g., WebAssembly.compileStreaming),
* Node's error handling calls the custom function with host context, enabling escape.
*
* - 'nodejs.rejection': Called by EventEmitter on promise rejection with captureRejections enabled.
* The handler receives error objects that could potentially leak host context.
*
* Fix: Override Symbol.for to return sandbox-local symbols for dangerous keys instead of cross-realm
* symbols. This prevents Node.js internals from recognizing sandbox-defined symbol properties while
* preserving cross-realm behavior for other symbols.
*/
const originalSymbolFor = Symbol.for;
const blockedSymbolCustomInspect = Symbol('nodejs.util.inspect.custom');
const blockedSymbolRejection = Symbol('nodejs.rejection');
Symbol.for = function(key) {
// Convert to string once to prevent toString/toPrimitive bypass and TOCTOU attacks
const keyStr = '' + key;
if (keyStr === 'nodejs.util.inspect.custom') {
return blockedSymbolCustomInspect;
}
if (keyStr === 'nodejs.rejection') {
return blockedSymbolRejection;
}
return originalSymbolFor(keyStr);
};
/*
* Cross-realm symbol extraction protection
*
* Even with Symbol.for overridden, cross-realm symbols can be extracted from
* host objects exposed to the sandbox (e.g., Buffer.prototype) via:
* Object.getOwnPropertySymbols(Buffer.prototype).find(s => s.description === 'nodejs.util.inspect.custom')
*
* Fix: Override Object.getOwnPropertySymbols and Reflect.ownKeys to replace
* dangerous cross-realm symbols with sandbox-local equivalents in results.
*/
const realSymbolCustomInspect = originalSymbolFor('nodejs.util.inspect.custom');
const realSymbolRejection = originalSymbolFor('nodejs.rejection');
function isDangerousSymbol(sym) {
return sym === realSymbolCustomInspect || sym === realSymbolRejection;
}
localObject.getOwnPropertySymbols = function getOwnPropertySymbols(obj) {
const symbols = apply(localObjectGetOwnPropertySymbols, localObject, [obj]);
const result = [];
let j = 0;
for (let i = 0; i < symbols.length; i++) {
if (typeof symbols[i] !== 'symbol' || !isDangerousSymbol(symbols[i])) {
localReflectDefineProperty(result, j++, {
__proto__: null,
value: symbols[i],
writable: true,
enumerable: true,
configurable: true
});
}
}
return result;
};
localReflect.ownKeys = function ownKeys(obj) {
const keys = apply(localReflectOwnKeys, localReflect, [obj]);
const result = [];
let j = 0;
for (let i = 0; i < keys.length; i++) {
if (typeof keys[i] !== 'symbol' || !isDangerousSymbol(keys[i])) {
localReflectDefineProperty(result, j++, {
__proto__: null,
value: keys[i],
writable: true,
enumerable: true,
configurable: true
});
}
}
return result;
};
/*
* Object.getOwnPropertyDescriptors uses the internal [[OwnPropertyKeys]] which
* bypasses our Reflect.ownKeys override. The result object has dangerous symbols
* as property keys, which can then be leaked via Object.assign/Object.defineProperties
* to a Proxy whose set/defineProperty trap captures the key.
*/
localObject.getOwnPropertyDescriptors = function getOwnPropertyDescriptors(obj) {
const descs = apply(localObjectGetOwnPropertyDescriptors, localObject, [obj]);
localReflectDeleteProperty(descs, realSymbolCustomInspect);
localReflectDeleteProperty(descs, realSymbolRejection);
return descs;
};
/*
* Object.assign uses internal [[OwnPropertyKeys]] on source objects, bypassing our
* Reflect.ownKeys override. If a source (bridge proxy) has an enumerable dangerous-symbol
* property, the symbol is passed to the target's [[Set]] which could be a user Proxy trap.
*/
localObject.assign = function assign(target) {
if (target === null || target === undefined) {
throw new LocalError('Cannot convert undefined or null to object');
}
const to = localObject(target);
for (let s = 1; s < arguments.length; s++) {
const source = arguments[s];
if (source === null || source === undefined) continue;
const from = localObject(source);
const keys = apply(localReflectOwnKeys, localReflect, [from]);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (typeof key === 'symbol' && isDangerousSymbol(key)) continue;
const desc = apply(localReflectGetOwnPropertyDescriptor, localReflect, [from, key]);
if (desc && desc.enumerable === true) {
to[key] = from[key];
}
}
}
return to;
};
const resetPromiseSpecies = (p) => {
if (p instanceof globalPromise && ![globalPromise, localPromise].includes(p.constructor[speciesSymbol])) {
Object.defineProperty(p.constructor, speciesSymbol, { value: localPromise });
if (p instanceof globalPromise) {
// Always define an own data property for 'constructor' to eliminate
// any TOCTOU vulnerability. Accessor properties (getters) on either the
// instance or anywhere in the prototype chain can return different values
// on each access, allowing an attacker to pass our check on the first read
// while V8 internally sees a malicious species on subsequent reads.
if (!localReflectDefineProperty(p, 'constructor', { __proto__: null, value: localPromise, writable: true, configurable: true })) {
throw new LocalError('Unsafe Promise species cannot be reset');
}
}

@@ -610,2 +747,26 @@ };

// Secure Promise.try to prevent species attacks via static method stealing.
// Promise.try is uniquely vulnerable because it catches errors thrown by the callback
// INSIDE V8's Promise executor, passing them directly to the FakePromise's reject
// handler without going through bridge sanitization or transformer-instrumented catch blocks.
//
// Other Promise static methods are NOT vulnerable:
// - Promise.reject/withResolvers: errors come from user catch blocks (transformer-sanitized)
// - Promise.all/race/any/allSettled: use .then() internally (wrapped with ensureThis)
// - Promise.resolve: FakePromise doesn't implement proper thenable resolution
//
// We wrap Promise.try to always use localPromise as constructor regardless of `this`.
const globalPromiseTry = globalPromise.try;
if (typeof globalPromiseTry === 'function') {
globalPromise.try = function _try() {
return apply(globalPromiseTry, localPromise, arguments);
};
}
// Freeze globalPromise to prevent Symbol.hasInstance override
// (which would bypass the instanceof check in resetPromiseSpecies).
// Freeze globalPromise.prototype to prevent defining accessor properties
// on 'constructor' that could be used for TOCTOU attacks via the prototype chain.
Object.freeze(globalPromise);
Object.freeze(globalPromise.prototype);
Object.freeze(localPromise);

@@ -612,0 +773,0 @@ Object.freeze(PromisePrototype);

+1
-1

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

],
"version": "3.10.2",
"version": "3.10.3",
"main": "index.js",

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