+60
-2
@@ -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) => { |
+1
-1
@@ -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 ` + |
+15
-0
@@ -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; |
+9
-2
@@ -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); |
+2
-2
@@ -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; |
+1
-1
@@ -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; |
+529
-167
@@ -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, | ||
| }; |
+34
-2
@@ -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; |
+3
-3
@@ -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 ." |
+60
-1
@@ -145,2 +145,3 @@ # vm2 [![NPM Version][npm-image]][npm-url] [![NPM Downloads][downloads-image]][downloads-url] [![License][license-image]][license-url] [](https://github.com/patriksimek/vm2/actions/workflows/test.yml) [![Known Vulnerabilities][snyk-image]][snyk-url] | ||
| - `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
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
321687
31.81%7844
23%521
12.77%