+27
-1
@@ -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 @@ |
+164
-3
@@ -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, |
226028
3.48%6085
2.98%