VYPR
Moderate severityNVD Advisory· Published Mar 18, 2026· Updated Mar 19, 2026

SandboxJS timers have an execution-quota bypass (cross-sandbox currentTicks race)

CVE-2026-32723

Description

SandboxJS is a JavaScript sandboxing library. Prior to 0.8.35, SandboxJS timers have an execution-quota bypass. A global tick state (currentTicks.current) is shared between sandboxes. Timer string handlers are compiled at execution time using that global tick state rather than the scheduling sandbox's tick object. In multi-tenant / concurrent sandbox scenarios, another sandbox can overwrite currentTicks.current between scheduling and execution, causing the timer callback to run under a different sandbox's tick budget and bypass the original sandbox's execution quota/watchdog. Version 0.8.35 fixes this issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@nyariv/sandboxjsnpm
< 0.8.350.8.35

Affected products

1

Patches

1
cc8f20b4928a

Merge commit from fork

https://github.com/nyariv/SandboxJSnyarivMar 14, 2026via ghsa
6 files changed · +63 21
  • .npmignore+2 1 modified
    @@ -14,4 +14,5 @@ rollup.config.mjs
     .prettierrc
     jest.config.js
     rollup.config.js
    -tsconfig.jest.json
    \ No newline at end of file
    +tsconfig.jest.json
    +package-lock.json
    
  • package.json+1 1 modified
    @@ -1,6 +1,6 @@
     {
       "name": "@nyariv/sandboxjs",
    -  "version": "0.8.34",
    +  "version": "0.8.35",
       "description": "Javascript sandboxing library.",
       "main": "dist/node/Sandbox.js",
       "module": "./build/Sandbox.js",
    
  • package-lock.json+20 10 modified
    @@ -1,12 +1,12 @@
     {
       "name": "@nyariv/sandboxjs",
    -  "version": "0.8.33",
    +  "version": "0.8.35",
       "lockfileVersion": 3,
       "requires": true,
       "packages": {
         "": {
           "name": "@nyariv/sandboxjs",
    -      "version": "0.8.33",
    +      "version": "0.8.35",
           "license": "MIT",
           "devDependencies": {
             "@rollup/plugin-node-resolve": "^16.0.3",
    @@ -60,6 +60,7 @@
           "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
           "dev": true,
           "license": "MIT",
    +      "peer": true,
           "dependencies": {
             "@babel/code-frame": "^7.28.6",
             "@babel/generator": "^7.28.6",
    @@ -2130,6 +2131,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",
    @@ -2610,6 +2612,7 @@
           "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
           "dev": true,
           "license": "MIT",
    +      "peer": true,
           "bin": {
             "acorn": "bin/acorn"
           },
    @@ -2628,9 +2631,9 @@
           }
         },
         "node_modules/ajv": {
    -      "version": "6.12.6",
    -      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
    -      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
    +      "version": "6.14.0",
    +      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
    +      "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
           "dev": true,
           "license": "MIT",
           "dependencies": {
    @@ -2882,6 +2885,7 @@
             }
           ],
           "license": "MIT",
    +      "peer": true,
           "dependencies": {
             "baseline-browser-mapping": "^2.9.0",
             "caniuse-lite": "^1.0.30001759",
    @@ -3402,6 +3406,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",
    @@ -3888,9 +3893,9 @@
           }
         },
         "node_modules/flatted": {
    -      "version": "3.3.3",
    -      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
    -      "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
    +      "version": "3.4.1",
    +      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
    +      "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
           "dev": true,
           "license": "ISC"
         },
    @@ -4416,6 +4421,7 @@
           "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
           "dev": true,
           "license": "MIT",
    +      "peer": true,
           "dependencies": {
             "@jest/core": "30.2.0",
             "@jest/types": "30.2.0",
    @@ -5235,7 +5241,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",
    @@ -6102,6 +6109,7 @@
           "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
           "dev": true,
           "license": "MIT",
    +      "peer": true,
           "dependencies": {
             "@types/estree": "1.0.8"
           },
    @@ -6882,7 +6890,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",
    @@ -6926,6 +6935,7 @@
           "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
           "dev": true,
           "license": "Apache-2.0",
    +      "peer": true,
           "bin": {
             "tsc": "bin/tsc",
             "tsserver": "bin/tsserver"
    
  • src/eval.ts+6 6 modified
    @@ -1,4 +1,4 @@
    -import { createFunction, createFunctionAsync, currentTicks } from './executor.js';
    +import { createFunction, createFunctionAsync } from './executor.js';
     import parse, { Lisp, lispifyFunction } from './parser.js';
     import { IExecContext, LispType, Ticks } from './utils.js';
     
    @@ -41,7 +41,7 @@ export function createEvalContext(): IEvalContext {
     }
     
     function SB() {}
    -export function sandboxFunction(context: IExecContext, ticks?: Ticks): SandboxFunction {
    +export function sandboxFunction(context: IExecContext): SandboxFunction {
       SandboxFunction.prototype = SB.prototype;
       return SandboxFunction;
       function SandboxFunction(...params: string[]) {
    @@ -50,7 +50,7 @@ export function sandboxFunction(context: IExecContext, ticks?: Ticks): SandboxFu
         return createFunction(
           params,
           parsed.tree,
    -      ticks || currentTicks.current,
    +      context.ctx.ticks,
           {
             ...context,
             constants: parsed.constants,
    @@ -64,7 +64,7 @@ export function sandboxFunction(context: IExecContext, ticks?: Ticks): SandboxFu
     
     export type SandboxAsyncFunction = (code: string, ...args: string[]) => () => Promise<unknown>;
     function SAF() {}
    -export function sandboxAsyncFunction(context: IExecContext, ticks?: Ticks): SandboxAsyncFunction {
    +export function sandboxAsyncFunction(context: IExecContext): SandboxAsyncFunction {
       SandboxAsyncFunction.prototype = SAF.prototype;
       return SandboxAsyncFunction;
       function SandboxAsyncFunction(...params: string[]) {
    @@ -73,7 +73,7 @@ export function sandboxAsyncFunction(context: IExecContext, ticks?: Ticks): Sand
         return createFunctionAsync(
           params,
           parsed.tree,
    -      ticks || currentTicks.current,
    +      context.ctx.ticks,
           {
             ...context,
             constants: parsed.constants,
    @@ -97,7 +97,7 @@ export function sandboxedEval(func: SandboxFunction, context: IExecContext): San
         return createFunction(
           [],
           tree,
    -      currentTicks.current,
    +      context.ctx.ticks,
           {
             ...context,
             constants: parsed.constants,
    
  • src/executor.ts+0 3 modified
    @@ -1697,8 +1697,6 @@ const unexecTypes = new Set([
       LispType.Typeof,
     ]);
     
    -export const currentTicks = { current: { ticks: BigInt(0) } as Ticks };
    -
     function _execNoneRecurse<T = any>(
       ticks: Ticks,
       tree: LispItem,
    @@ -1709,7 +1707,6 @@ function _execNoneRecurse<T = any>(
       inLoopOrSwitch?: string,
     ): boolean {
       const exec = isAsync ? execAsync : execSync;
    -  currentTicks.current = ticks;
       if (tree instanceof Prop) {
         done(undefined, tree.get(context));
       } else if (tree === optional) {
    
  • test/ticksQuotaHalt.spec.ts+34 0 modified
    @@ -58,4 +58,38 @@ describe('ticks quota halt', () => {
         expect(haltCalled).toBe(true);
         expect(len).toBe(100);
       });
    +
    +  it('should halt sandbox A when execution quota is exceeded', (done) => {
    +    const globals = { ...Sandbox.SAFE_GLOBALS, setTimeout, clearTimeout };
    +    const prototypeWhitelist = Sandbox.SAFE_PROTOTYPES;
    +
    +    const sandboxA = new Sandbox({
    +      globals,
    +      prototypeWhitelist,
    +      executionQuota: 50n,
    +      haltOnSandboxError: true,
    +    });
    +    let haltedA = false;
    +    sandboxA.subscribeHalt(() => {
    +      haltedA = true;
    +    });
    +
    +    const sandboxB = new Sandbox({ globals, prototypeWhitelist });
    +
    +    // Sandbox A schedules a heavy string handler
    +    sandboxA
    +      .compile(
    +        'setTimeout("let x=0; for (let i=0;i<200;i++){ x += i } globalThis.doneA = true;", 0);',
    +      )()
    +      .run();
    +
    +    // Run sandbox B before A's timer fires
    +    sandboxB.compile('1+1')().run();
    +
    +    setTimeout(() => {
    +      expect(haltedA).toBe(true);
    +      expect(sandboxA.context.sandboxGlobal.doneA).toBeUndefined();
    +      done();
    +    }, 50);
    +  });
     });
    

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

News mentions

0

No linked articles in our index yet.