@nyariv/sandboxjs has host prototype pollution from sandbox via array intermediary (sandbox escape)
Description
SandboxJS is a JavaScript sandboxing library. Prior to 0.8.31, a sandbox escape vulnerability allows sandboxed code to mutate host built-in prototypes by laundering the isGlobal protection flag through array literal intermediaries. When a global prototype reference (e.g., Map.prototype, Set.prototype) is placed into an array and retrieved, the isGlobal taint is stripped, permitting direct prototype mutation from within the sandbox. This results in persistent host-side prototype pollution and may enable RCE in applications that use polluted properties in sensitive sinks (example gadget: execSync(obj.cmd)). This vulnerability is fixed in 0.8.31.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@nyariv/sandboxjsnpm | < 0.8.31 | 0.8.31 |
Affected products
1Patches
1f369f8db2664fix(security): harden sandbox against code execution bypass (GHSA-ww7g-4gwx-m7wj)
27 files changed · +302 −154
eslint.config.js+1 −1 modified@@ -46,7 +46,7 @@ module.exports = [ '@typescript-eslint/no-unused-expressions': 'warn', '@typescript-eslint/no-unsafe-function-type': 'off', 'no-fallthrough': 'off', - '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^lastLastLastLastPart' }], + '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^lastLastLastLastPart' }], '@typescript-eslint/no-unsafe-assignment': 'off', } }
package.json+1 −1 modified@@ -1,6 +1,6 @@ { "name": "@nyariv/sandboxjs", - "version": "0.8.30", + "version": "0.8.31", "description": "Javascript sandboxing library.", "main": "dist/node/Sandbox.js", "module": "./build/Sandbox.js",
package-lock.json+14 −4 modified@@ -1,12 +1,12 @@ { "name": "@nyariv/sandboxjs", - "version": "0.8.28", + "version": "0.8.31", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nyariv/sandboxjs", - "version": "0.8.28", + "version": "0.8.31", "license": "MIT", "devDependencies": { "@rollup/plugin-node-resolve": "^16.0.3", @@ -58,6 +58,7 @@ "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -2128,6 +2129,7 @@ "integrity": "sha512-npiaib8XzbjtzS2N4HlqPvlpxpmZ14FjSJrteZpPxGUaYPlvhzlzUZ4mZyABo0EFrOWnvyd0Xxroq//hKhtAWg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", @@ -2608,6 +2610,7 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2880,6 +2883,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -3330,6 +3334,7 @@ "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4308,6 +4313,7 @@ "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "30.2.0", "@jest/types": "30.2.0", @@ -5018,7 +5024,8 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.memoize": { "version": "4.1.2", @@ -5724,6 +5731,7 @@ "integrity": "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -6469,7 +6477,8 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true, - "license": "0BSD" + "license": "0BSD", + "peer": true }, "node_modules/type-check": { "version": "0.4.0", @@ -6513,6 +6522,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver"
src/eval.ts+17 −6 modified@@ -98,14 +98,14 @@ export function sandboxedSetTimeout( const h = typeof handler === 'string' ? func(handler) : handler; haltsub.unsubscribe(); contsub.unsubscribe(); + sandbox.setTimeoutHandles.delete(sandBoxhandle); return h(...a); }; const sandBoxhandle = ++sandbox.timeoutHandleCounter; let start = Date.now(); - let handle: any = setTimeout(exec, timeout, ...args); - sandbox.setTimeoutHandles.set(sandBoxhandle, handle); + let handle: number = setTimeout(exec, timeout, ...args); let elapsed = 0; const haltsub = sandbox.subscribeHalt(() => { @@ -116,7 +116,16 @@ export function sandboxedSetTimeout( start = Date.now(); const remaining = Math.floor((timeout || 0) - elapsed); handle = setTimeout(exec, remaining, ...args); - sandbox.setTimeoutHandles.set(sandBoxhandle, handle); + sandbox.setTimeoutHandles.set(sandBoxhandle, { + handle, + haltsub, + contsub, + }); + }); + sandbox.setTimeoutHandles.set(sandBoxhandle, { + handle, + haltsub, + contsub, }); return sandBoxhandle; }; @@ -127,7 +136,9 @@ export function sandboxedClearTimeout(context: IExecContext): SandboxClearTimeou const sandbox = context.ctx.sandbox; const timeoutHandle = sandbox.setTimeoutHandles.get(handle); if (timeoutHandle) { - clearTimeout(timeoutHandle); + clearTimeout(timeoutHandle.handle); + timeoutHandle.haltsub.unsubscribe(); + timeoutHandle.contsub.unsubscribe(); sandbox.setTimeoutHandles.delete(handle); } }; @@ -138,9 +149,9 @@ export function sandboxedClearInterval(context: IExecContext): SandboxClearInter const intervalHandle = sandbox.setIntervalHandles.get(handle); if (intervalHandle) { clearInterval(intervalHandle.handle); - sandbox.setIntervalHandles.delete(handle); intervalHandle.haltsub.unsubscribe(); intervalHandle.contsub.unsubscribe(); + sandbox.setIntervalHandles.delete(handle); } }; } @@ -161,7 +172,7 @@ export function sandboxedSetInterval( const sandBoxhandle = ++sandbox.timeoutHandleCounter; let start = Date.now(); - let handle: any = setInterval(exec, timeout, ...args); + let handle: number = setInterval(exec, timeout, ...args); let elapsed = 0; const haltsub = sandbox.subscribeHalt(() => {
src/executor.ts+95 −96 modified@@ -122,7 +122,6 @@ function generateArgs(argNames: string[], args: unknown[]) { return vars; } -export const sandboxedFunctions = new WeakSet(); export function createFunction( argNames: string[], parsed: Lisp[], @@ -159,7 +158,7 @@ export function createFunction( }; } context.registerSandboxFunction(func); - sandboxedFunctions.add(func); + context.ctx.sandboxedFunctions.add(func); return func; } @@ -202,7 +201,7 @@ export function createFunctionAsync( }; } context.registerSandboxFunction(func); - sandboxedFunctions.add(func); + context.ctx.sandboxedFunctions.add(func); return func; } @@ -339,40 +338,9 @@ addOps<unknown, PropertyKey>(LispType.Prop, ({ done, a, b, obj, context, scope } } } const val = prop.context ? (prop.context as any)[prop.prop] : undefined; - if (val === globalThis) { - done( - undefined, - new Prop( - { - [prop.prop]: context.ctx.sandboxGlobal, - }, - prop.prop, - prop.isConst, - false, - prop.isVariable, - ), - ); - return; - } + const p = getGlobalProp(val, context, prop) || prop; - const e = typeof val === 'function' && context.evals.get(val); - if (e) { - done( - undefined, - new Prop( - { - [prop.prop]: e, - }, - prop.prop, - prop.isConst, - true, - prop.isVariable, - ), - ); - return; - } - - done(undefined, prop); + done(undefined, p); return; } else if (a === undefined) { throw new TypeError(`Cannot read properties of undefined (reading '${b.toString()}')`); @@ -409,7 +377,10 @@ addOps<unknown, PropertyKey>(LispType.Prop, ({ done, a, b, obj, context, scope } done(undefined, new Prop(replace(a, true), b)); return; } - if (!(whitelist && (!whitelist.size || whitelist.has(b)))) { + if ( + !(whitelist && (!whitelist.size || whitelist.has(b))) && + !context.ctx.sandboxedFunctions.has(a) + ) { throw new SandboxAccessError( `Static method or property access not permitted: ${a.name}.${b.toString()}`, ); @@ -419,64 +390,89 @@ addOps<unknown, PropertyKey>(LispType.Prop, ({ done, a, b, obj, context, scope } let prot: {} = a; while ((prot = Object.getPrototypeOf(prot))) { - if (hasOwnProperty(prot, b)) { + if (hasOwnProperty(prot, b) || b === '__proto__') { const whitelist = context.ctx.prototypeWhitelist.get(prot); const replace = context.ctx.options.prototypeReplacements.get(prot.constructor); if (replace) { done(undefined, new Prop(replace(a, false), b)); return; } - if (whitelist && (!whitelist.size || whitelist.has(b))) { + if ( + (whitelist && (!whitelist.size || whitelist.has(b))) || + context.ctx.sandboxedFunctions.has(prot.constructor) + ) { break; } + if (b === '__proto__') { + throw new SandboxAccessError(`Access to prototype of global object is not permitted`); + } throw new SandboxAccessError( `Method or property access not permitted: ${prot.constructor.name}.${b.toString()}`, ); } } } - const e = - typeof a[b as keyof typeof a] === 'function' && context.evals.get(a[b as keyof typeof a]); - if (e) { - done( - undefined, - new Prop( - { - [b]: e, - }, - b, - false, - true, - false, - ), - ); - return; + const val = a[b as keyof typeof a] as unknown; + if (typeof a === 'function') { + if (b === 'prototype' && !context.ctx.sandboxedFunctions.has(a)) { + throw new SandboxAccessError(`Access to prototype of global object is not permitted`); + } } - if (a[b as keyof typeof a] === globalThis) { - done( - undefined, - new Prop( - { - [b]: context.ctx.sandboxGlobal, - }, - b, - false, - false, - false, - ), - ); + + if (b === '__proto__' && !context.ctx.sandboxedFunctions.has(val?.constructor as any)) { + throw new SandboxAccessError(`Access to prototype of global object is not permitted`); + } + + const p = getGlobalProp(val, context); + if (p) { + done(undefined, p); return; } const g = (obj instanceof Prop && obj.isGlobal) || - (typeof a === 'function' && !sandboxedFunctions.has(a)) || + (typeof a === 'function' && !context.ctx.sandboxedFunctions.has(a)) || context.ctx.globalsWhitelist.has(a); done(undefined, new Prop(a, b, false, g, false)); }); +function getGlobalProp(val: unknown, context: IExecContext, prop?: Prop) { + if (!val) return; + const isFunc = typeof val === 'function'; + if (val instanceof Prop) { + if (!prop) { + prop = val; + } + val = val.get(context); + } + const p = prop?.prop || 'prop'; + if (val === globalThis) { + return new Prop( + { + [p]: context.ctx.sandboxGlobal, + }, + p, + prop?.isConst || false, + false, + prop?.isVariable || false, + ); + } + const e = isFunc && context.evals.get(val); + if (e) { + return new Prop( + { + [p]: e, + }, + p, + prop?.isConst || false, + true, + prop?.isVariable || false, + ); + } +} + addOps<unknown, Lisp[], any>(LispType.Call, ({ done, a, b, obj, context }) => { if (context.ctx.options.forbidFunctionCalls) throw new SandboxCapabilityError('Function invocations are not allowed'); @@ -498,6 +494,8 @@ addOps<unknown, Lisp[], any>(LispType.Call, ({ done, a, b, obj, context }) => { let ret = obj(...vals); if (ret instanceof Promise) { ret = checkHaltAsync(context, ret); + } else { + ret = getGlobalProp(ret, context) || ret; } done(undefined, ret); return; @@ -550,10 +548,13 @@ addOps<unknown, Lisp[], any>(LispType.Call, ({ done, a, b, obj, context }) => { } else if (obj.prop === 'splice') { change = { type: 'splice', - startIndex: vals[0], + startIndex: vals[0] as number, deleteCount: vals[1] === undefined ? obj.context.length : vals[1], added: vals.slice(2), - removed: obj.context.slice(vals[0], vals[1] === undefined ? undefined : vals[0] + vals[1]), + removed: obj.context.slice( + vals[0], + vals[1] === undefined ? undefined : (vals[0] as number) + (vals[1] as number), + ), }; changed = !!change.added.length || !!change.removed.length; } else if (obj.prop === 'reverse' || obj.prop === 'sort') { @@ -562,14 +563,14 @@ addOps<unknown, Lisp[], any>(LispType.Call, ({ done, a, b, obj, context }) => { } else if (obj.prop === 'copyWithin') { const len = vals[2] === undefined - ? obj.context.length - vals[1] - : Math.min(obj.context.length, vals[2] - vals[1]); + ? obj.context.length - (vals[1] as number) + : Math.min(obj.context.length, (vals[2] as number) - (vals[1] as number)); change = { type: 'copyWithin', - startIndex: vals[0], - endIndex: vals[0] + len, - added: obj.context.slice(vals[1], vals[1] + len), - removed: obj.context.slice(vals[0], vals[0] + len), + startIndex: vals[0] as number, + endIndex: (vals[0] as number) + len, + added: obj.context.slice(vals[1] as number, (vals[1] as number) + len), + removed: obj.context.slice(vals[0] as number, (vals[0] as number) + len), }; changed = !!change.added.length || !!change.removed.length; } @@ -579,15 +580,11 @@ addOps<unknown, Lisp[], any>(LispType.Call, ({ done, a, b, obj, context }) => { } } obj.get(context); - let ret = obj.context[obj.prop](...vals); - if (typeof ret === 'function') { - ret = context.evals.get(ret) || ret; - } - if (ret === globalThis) { - ret = context.ctx.sandboxGlobal; - } + let ret = obj.context[obj.prop](...vals) as unknown; if (ret instanceof Promise) { ret = checkHaltAsync(context, ret); + } else { + ret = getGlobalProp(ret, context) || ret; } done(undefined, ret); }); @@ -1162,7 +1159,7 @@ addOps(LispType.Void, ({ done }) => { done(); }); addOps<new (...args: unknown[]) => unknown, unknown[]>(LispType.New, ({ done, a, b, context }) => { - if (!context.ctx.globalsWhitelist.has(a) && !sandboxedFunctions.has(a)) { + if (!context.ctx.globalsWhitelist.has(a) && !context.ctx.sandboxedFunctions.has(a)) { throw new SandboxAccessError(`Object construction not allowed: ${a.constructor.name}`); } done(undefined, new a(...b)); @@ -1174,7 +1171,7 @@ addOps(LispType.Throw, ({ done, b }) => { addOps<unknown[]>(LispType.Expression, ({ done, a }) => done(undefined, a.pop())); addOps(LispType.None, ({ done }) => done()); -function valueOrProp(a: unknown, context: IExecContext): any { +function valueOrProp(a: unknown, context: IExecContext): unknown { if (a instanceof Prop) return a.get(context); if (a === optional) return undefined; return a; @@ -1481,8 +1478,7 @@ export function execSync<T = any>( } } -async function checkHaltAsync(context: IExecContext, promise: unknown) { - if (!(promise instanceof Promise)) return promise; +function checkHaltAsync<T>(context: IExecContext, promise: Promise<T>): Promise<T> { let done = false; let halted = context.ctx.sandbox.halted; let doResolve = () => {}; @@ -1503,14 +1499,17 @@ async function checkHaltAsync(context: IExecContext, promise: unknown) { if (done) doResolve(); }); }); - promise.finally(() => { - done = true; - if (!halted) { - doResolve(); - } + promise + .finally(() => { + done = true; + if (!halted) { + doResolve(); + } + }) + .catch(() => {}); + return Promise.allSettled([promise, interupted]).then(() => { + return promise; }); - await Promise.allSettled([promise, interupted]); - return promise; } type OpsCallbackParams<a, b, obj, bobj> = { @@ -1692,7 +1691,7 @@ function _execNoneRecurse<T = any>( if (e) done(e); else try { - done(undefined, await valueOrProp(r, context)); + done(undefined, (await valueOrProp(r, context)) as any); } catch (err) { done(err); }
src/SandboxExec.ts+8 −4 modified@@ -70,18 +70,22 @@ export default class SandboxExec { SubscriptionSubject, Set<(modification: Change) => void> > = new WeakMap(); - public readonly sandboxFunctions: WeakMap<(...args: any[]) => any, IExecContext> = new WeakMap(); + public readonly sandboxFunctions: WeakMap<Function, IExecContext> = new WeakMap(); private haltSubscriptions: Set< (args?: { error: Error; ticks: Ticks; scope: Scope; context: IExecContext }) => void > = new Set(); private resumeSubscriptions: Set<() => void> = new Set(); public halted = false; timeoutHandleCounter = 0; - public readonly setTimeoutHandles = new Map<number, ReturnType<typeof setTimeout>>(); + public readonly setTimeoutHandles = new Map<number, { + handle: number, + haltsub: { unsubscribe: () => void }, + contsub: { unsubscribe: () => void }, + }>(); public readonly setIntervalHandles = new Map< number, { - handle: ReturnType<typeof setInterval>; + handle: number; haltsub: { unsubscribe: () => void }; contsub: { unsubscribe: () => void }; } @@ -272,7 +276,7 @@ export default class SandboxExec { resumeExecution() { if (!this.halted) return; - if (this.context.ticks.ticks >= this.context.ticks.tickLimit!) { + if (this.context.ticks.tickLimit && this.context.ticks.ticks >= this.context.ticks.tickLimit) { throw new SandboxExecutionQuotaExceededError('Cannot resume execution: tick limit exceeded'); } this.halted = false;
src/utils.ts+4 −2 modified@@ -40,6 +40,7 @@ export interface IContext { sandboxGlobal: ISandboxGlobal; globalsWhitelist: Set<any>; prototypeWhitelist: Map<any, Set<PropertyKey>>; + sandboxedFunctions: WeakSet<Function>; options: IOptions; auditReport?: IAuditReport; ticks: Ticks; @@ -124,6 +125,7 @@ export function createContext(sandbox: SandboxExec, options: IOptions): IContext globalScope: new Scope(null, options.globals, sandboxGlobal), sandboxGlobal, ticks: { ticks: 0n, tickLimit: options.executionQuota }, + sandboxedFunctions: new WeakSet<Function>(), }; context.prototypeWhitelist.set(Object.getPrototypeOf([][Symbol.iterator]()) as object, new Set()); return context; @@ -388,14 +390,14 @@ export class Scope { return this.parent?.getWhereValScope(key, isThis) || null; } } - if (key in this.allVars) { + if (key in this.allVars && !(key in {} && !hasOwnProperty(this.allVars, key))) { return this; } return this.parent?.getWhereValScope(key, isThis) || null; } getWhereVarScope(key: string, localScope = false): Scope { - if (key in this.allVars) { + if (key in this.allVars && !(key in {} && !hasOwnProperty(this.allVars, key))) { return this; } if (this.parent === null || localScope || this.functionThis !== undefined) {
test/eval/testCases/arithmetic-operators.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './arithmetic-operators.data.js';
test/eval/testCases/assignment-operators.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './assignment-operators.data.js';
test/eval/testCases/bitwise-operators.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './bitwise-operators.data.js';
test/eval/testCases/comments.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './comments.data.js';
test/eval/testCases/comparison-operators.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './comparison-operators.data.js';
test/eval/testCases/complex-expressions.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './complex-expressions.data.js';
test/eval/testCases/conditionals.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './conditionals.data.js';
test/eval/testCases/data-types.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './data-types.data.js';
test/eval/testCases/error-handling.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './error-handling.data.js';
test/eval/testCases/functions.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './functions.data.js';
test/eval/testCases/logical-operators.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './logical-operators.data.js';
test/eval/testCases/loops.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './loops.data.js';
test/eval/testCases/objects-and-arrays.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './objects-and-arrays.data.js';
test/eval/testCases/operator-precedence.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './operator-precedence.data.js';
test/eval/testCases/other-operators.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './other-operators.data.js';
test/eval/testCases/security.data.ts+39 −3 modified@@ -156,19 +156,19 @@ export const tests: TestCase[] = [ { code: '[].constructor.prototype.flatMap = 1', evalExpect: 1, - safeExpect: "/Cannot assign property 'flatMap' of a global object/", + safeExpect: '/Access to prototype of global object is not permitted/', category: 'Security', }, { code: '[].__proto__.flatMap = 1', evalExpect: 1, - safeExpect: '/Method or property access not permitted/', + safeExpect: '/Access to prototype of global object is not permitted/', category: 'Security', }, { code: 'let p; p = [].constructor.prototype; p.flatMap = 2; return [].flatMap;', evalExpect: 2, - safeExpect: "/Cannot assign property 'flatMap' of a global object/", + safeExpect: '/Access to prototype of global object is not permitted/', category: 'Security', }, { @@ -237,4 +237,40 @@ export const tests: TestCase[] = [ safeExpect: '/Method or property access not permitted/', category: 'Security', }, + { + code: `const arr=[Array.prototype]; arr[0].polluted = 1; return [].polluted;`, + evalExpect: 1, + safeExpect: '/Access to prototype of global object is not permitted/', + category: 'Security', + }, + { + code: `(async () => Array.prototype)().then((a) => a.polluted = 'pwned').then(() => [].polluted)`, + evalExpect: 'ok', + safeExpect: '/Access to prototype of global object is not permitted/', + category: 'Security', + }, + { + code: `function x() {}; x.prototype.permitted = true; return new x().permitted;`, + evalExpect: true, + safeExpect: true, + category: 'Security', + }, + { + code: `function x() {}; const y = new x(); y.__proto__.permitted = true; return y.permitted;`, + evalExpect: true, + safeExpect: true, + category: 'Security', + }, + { + code: `function x() {}; const y = new x(); y.__proto__.__proto__.forbidden = true; return y.forbidden;`, + evalExpect: true, + safeExpect: '/Access to prototype of global object is not permitted/', + category: 'Security', + }, + { + code: `hasOwnProperty = 1; return ({}).hasOwnProperty`, + evalExpect: 'error', + safeExpect: '/hasOwnProperty is not defined/', + category: 'Security', + }, ];
test/eval/testCases/security.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './security.data.js';
test/eval/testCases/switch.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './switch.data.js';
test/eval/testCases/template-literals.spec.ts+0 −1 modified@@ -1,5 +1,4 @@ 'use strict'; -import { TestCase } from './types.js'; import { run, getState } from './test-utils.js'; import { tests } from './template-literals.data.js';
test/eval/tests.json+123 −19 modified@@ -43,8 +43,22 @@ }, { "code": "[\"\\\\\", \"\\xd9\", \"\\n\", \"\\r\", \"\\u2028\", \"\\u2029\"]", - "evalExpect": ["\\", "Ù", "\n", "\r", " ", " "], - "safeExpect": ["\\", "Ù", "\n", "\r", " ", " "], + "evalExpect": [ + "\\", + "Ù", + "\n", + "\r", + " ", + " " + ], + "safeExpect": [ + "\\", + "Ù", + "\n", + "\r", + " ", + " " + ], "category": "Data Types" }, { @@ -369,19 +383,19 @@ { "code": "[].constructor.prototype.flatMap = 1", "evalExpect": 1, - "safeExpect": "/Cannot assign property 'flatMap' of a global object/", + "safeExpect": "/Access to prototype of global object is not permitted/", "category": "Security" }, { "code": "[].__proto__.flatMap = 1", "evalExpect": 1, - "safeExpect": "/Method or property access not permitted/", + "safeExpect": "/Access to prototype of global object is not permitted/", "category": "Security" }, { "code": "let p; p = [].constructor.prototype; p.flatMap = 2; return [].flatMap;", "evalExpect": 2, - "safeExpect": "/Cannot assign property 'flatMap' of a global object/", + "safeExpect": "/Access to prototype of global object is not permitted/", "category": "Security" }, { @@ -450,6 +464,42 @@ "safeExpect": "/Method or property access not permitted/", "category": "Security" }, + { + "code": "const arr=[Array.prototype]; arr[0].polluted = 1; return [].polluted;", + "evalExpect": 1, + "safeExpect": "/Access to prototype of global object is not permitted/", + "category": "Security" + }, + { + "code": "(async () => Array.prototype)().then((a) => a.polluted = 'pwned').then(() => [].polluted)", + "evalExpect": "ok", + "safeExpect": "/Access to prototype of global object is not permitted/", + "category": "Security" + }, + { + "code": "function x() {}; x.prototype.permitted = true; return new x().permitted;", + "evalExpect": true, + "safeExpect": true, + "category": "Security" + }, + { + "code": "function x() {}; const y = new x(); y.__proto__.permitted = true; return y.permitted;", + "evalExpect": true, + "safeExpect": true, + "category": "Security" + }, + { + "code": "function x() {}; const y = new x(); y.__proto__.__proto__.forbidden = true; return y.forbidden;", + "evalExpect": true, + "safeExpect": "/Access to prototype of global object is not permitted/", + "category": "Security" + }, + { + "code": "hasOwnProperty = 1; return ({}).hasOwnProperty", + "evalExpect": "error", + "safeExpect": "/hasOwnProperty is not defined/", + "category": "Security" + }, { "code": "1+1", "evalExpect": 2, @@ -1238,8 +1288,14 @@ }, { "code": "let list = [0, 1]; return list.sort((a, b) => (a < b) ? 1 : -1)", - "evalExpect": [1, 0], - "safeExpect": [1, 0], + "evalExpect": [ + 1, + 0 + ], + "safeExpect": [ + 1, + 0 + ], "category": "Functions" }, { @@ -1256,8 +1312,12 @@ }, { "code": "[0,1].filter((...args) => args[1])", - "evalExpect": [1], - "safeExpect": [1], + "evalExpect": [ + 1 + ], + "safeExpect": [ + 1 + ], "category": "Functions" }, { @@ -1712,8 +1772,14 @@ }, { "code": "[test2, 2]", - "evalExpect": [1, 2], - "safeExpect": [1, 2], + "evalExpect": [ + 1, + 2 + ], + "safeExpect": [ + 1, + 2 + ], "category": "Objects & Arrays" }, { @@ -1740,14 +1806,34 @@ }, { "code": "Object.keys({a:1})", - "evalExpect": ["a"], - "safeExpect": ["a"], + "evalExpect": [ + "a" + ], + "safeExpect": [ + "a" + ], "category": "Objects & Arrays" }, { "code": "[1, ...[2, [test2, 4]], 5]", - "evalExpect": [1, 2, [1, 4], 5], - "safeExpect": [1, 2, [1, 4], 5], + "evalExpect": [ + 1, + 2, + [ + 1, + 4 + ], + 5 + ], + "safeExpect": [ + 1, + 2, + [ + 1, + 4 + ], + 5 + ], "category": "Objects & Arrays" }, { @@ -1776,14 +1862,32 @@ }, { "code": "const arr1 = [1, 2]; const arr2 = [3, 4]; return [...arr1, ...arr2]", - "evalExpect": [1, 2, 3, 4], - "safeExpect": [1, 2, 3, 4], + "evalExpect": [ + 1, + 2, + 3, + 4 + ], + "safeExpect": [ + 1, + 2, + 3, + 4 + ], "category": "Objects & Arrays" }, { "code": "const a = [1]; const b = [2]; const c = [3]; return [...a, ...b, ...c]", - "evalExpect": [1, 2, 3], - "safeExpect": [1, 2, 3], + "evalExpect": [ + 1, + 2, + 3 + ], + "safeExpect": [ + 1, + 2, + 3 + ], "category": "Objects & Arrays" }, {
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/advisories/GHSA-ww7g-4gwx-m7wjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-25881ghsaADVISORY
- github.com/nyariv/SandboxJS/commit/f369f8db26649f212a6a9a2e7a1624cb2f705b53ghsax_refsource_MISCWEB
- github.com/nyariv/SandboxJS/security/advisories/GHSA-ww7g-4gwx-m7wjghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.