{"id":"CVE-2026-47135","aliases":["CLSA-2026-1787860907","EUVD-2026-36442","GHSA-2cm2-m3w5-gp2f","GHSA-m5q2-4fm3-vfqp","GHSA-m5w8-4gq2-6f8x","GHSA-q3fm-4wcw-g57x","GHSA-v836-6xw4-9cx3","ROOT-APP-NPM-CVE-2026-47135"],"status":"published","title":"vm2: Sandbox escape via unblocked cross-realm Symbol.for keys + missing bridge write-trap symbol checks","description":"vm2 is an open source vm/sandbox for Node.js. Prior to version 3.11.4, Symbol.for override in setup-sandbox.js only intercepts 2 of 9 dangerous Node.js cross-realm symbols. Combined with the bridge's set/defineProperty/deleteProperty traps having no isDangerousCrossRealmSymbol key check, sandbox code can obtain real cross-realm symbols, write them to host objects, and control host-side behavior — verified with a full util.promisify hijack chain. This issue has been patched in version 3.11.4.","severity":{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:N","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-11","score":0.00266,"percentile":0.18926,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-693"],"published":"2026-05-29T17:44:32Z","modified":"2026-10-28T02:30:40Z","fixes":[{"ecosystem":"npm","package":"vm2","fixed_in":["3.11.4"],"sources":["ghsa","osv"]},{"ecosystem":"Root:npm","package":"vm2","fixed_in":["3.9.17-aikido.11","3.9.17-aikido.12","3.11.3-aikido.1","3.11.3-aikido.2","3.9.17-aikido.13","3.11.3-aikido.3","3.9.17-aikido.7","3.9.17-aikido.8","3.9.17-aikido.9","3.9.17-aikido.10","3.9.7-aikido.3","3.9.7-aikido.4","3.9.7-aikido.5","3.9.7-aikido.6"],"sources":["osv"]},{"ecosystem":"Root:npm","package":"@rootio/vm2","fixed_in":["3.9.17-root.io.11","3.9.17-root.io.12","3.11.3-root.io.1","3.11.3-root.io.2","3.9.17-root.io.13","3.11.3-root.io.3","3.9.17-root.io.7","3.9.17-root.io.8","3.9.17-root.io.9","3.9.17-root.io.10","3.9.7-root.io.3","3.9.7-root.io.4","3.9.7-root.io.5","3.9.7-root.io.6"],"sources":["osv"]}],"affected":[{"source":"ghsa","ecosystem":"npm","package":"vm2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"last_affected":"3.11.3"},{"fixed":"3.11.4"}]}]},{"source":"osv","ecosystem":"TuxCare:npm","package":"vm2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"3.10.0-tuxcare.0"},{"fixed":"3.10.0-tuxcare.1"}]}],"versions":["3.10.0"],"bundled":30},{"source":"osv","ecosystem":"npm","package":"vm2","ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"3.11.4"}]}]},{"source":"osv","ecosystem":"Root:npm","package":"vm2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.9.17-aikido.11"},{"fixed":"3.9.17-aikido.12"},{"fixed":"3.11.3-aikido.1"},{"fixed":"3.11.3-aikido.2"},{"fixed":"3.9.17-aikido.13"},{"fixed":"3.11.3-aikido.3"},{"fixed":"3.9.17-aikido.7"},{"fixed":"3.9.17-aikido.8"},{"fixed":"3.9.17-aikido.9"},{"fixed":"3.9.17-aikido.10"},{"fixed":"3.9.7-aikido.3"},{"fixed":"3.9.7-aikido.4"},{"fixed":"3.9.7-aikido.5"},{"fixed":"3.9.7-aikido.6"}]}]},{"source":"osv","ecosystem":"Root:npm","package":"@rootio/vm2","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"3.9.17-root.io.11"},{"fixed":"3.9.17-root.io.12"},{"fixed":"3.11.3-root.io.1"},{"fixed":"3.11.3-root.io.2"},{"fixed":"3.9.17-root.io.13"},{"fixed":"3.11.3-root.io.3"},{"fixed":"3.9.17-root.io.7"},{"fixed":"3.9.17-root.io.8"},{"fixed":"3.9.17-root.io.9"},{"fixed":"3.9.17-root.io.10"},{"fixed":"3.9.7-root.io.3"},{"fixed":"3.9.7-root.io.4"},{"fixed":"3.9.7-root.io.5"},{"fixed":"3.9.7-root.io.6"}]}]}],"products":[{"vendor":"patriksimek","product":"vm2"}],"references":[{"url":"https://github.com/patriksimek/vm2/security/advisories/GHSA-m5q2-4fm3-vfqp","tags":["x_refsource_CONFIRM","advisory","web"]},{"url":"https://github.com/patriksimek/vm2/commit/928aef51898b5c52a05f05a40c4cfeb52e172878","tags":["x_refsource_MISC","fix","web"]},{"url":"https://github.com/patriksimek/vm2/releases/tag/v3.11.4","tags":["x_refsource_MISC","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-47135","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-m5q2-4fm3-vfqp"},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/47xxx/CVE-2026-47135.json","tags":["advisory"]},{"url":"https://github.com/patriksimek/vm2","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-47135","title":"vm2: Sandbox escape via unblocked cross-realm Symbol.for keys + missing bridge write-trap symbol checks","description":"vm2 is an open source vm/sandbox for Node.js. Prior to version 3.11.4, Symbol.for override in setup-sandbox.js only intercepts 2 of 9 dangerous Node.js cross-realm symbols. Combined with the bridge's set/defineProperty/deleteProperty traps having no isDangerousCrossRealmSymbol key check, sandbox code can obtain real cross-realm symbols, write them to host objects, and control host-side behavior — verified with a full util.promisify hijack chain. This issue has been patched in version 3.11.4.","status":"published","published":"2026-06-12T14:14:42.022Z","modified":"2026-06-13T03:55:55.641Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:N","level":"high","assessor":"cna"}],"cwes":["CWE-693"],"updated_at":"2026-11-02T13:36:40.478484Z"}],"euvd":[{"id":"EUVD-2026-36442","description":"vm2 is an open source vm/sandbox for Node.js. Prior to version 3.11.4, Symbol.for override in setup-sandbox.js only intercepts 2 of 9 dangerous Node.js cross-realm symbols. Combined with the bridge's set/defineProperty/deleteProperty traps having no isDangerousCrossRealmSymbol key check, sandbox code can obtain real cross-realm symbols, write them to host objects, and control host-side behavior — verified with a full util.promisify hijack chain. This issue has been patched in version 3.11.4.","status":"published","published":"2026-06-12T14:14:42Z","modified":"2026-06-13T03:55:55Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:N","level":"high"}],"updated_at":"2026-11-02T18:20:43.222Z"}],"ghsa":[{"id":"GHSA-m5q2-4fm3-vfqp","title":"vm2 has a sandbox escape via unblocked cross-realm Symbol.for keys + missing bridge write-trap symbol checks","description":"## Summary\n\nvm2 3.11.2 `Symbol.for` override in `setup-sandbox.js` only intercepts 2 of 9 dangerous Node.js cross-realm symbols. Combined with the bridge's `set`/`defineProperty`/`deleteProperty` traps having **no** `isDangerousCrossRealmSymbol` key check, sandbox code can obtain real cross-realm symbols, write them to host objects, and control host-side behavior — verified with a full `util.promisify` hijack chain.\n\n## Root Cause\n\n**1. Incomplete `Symbol.for` override** (`setup-sandbox.js:132-142`):\n\n```js\nSymbol.for = function (key) {\n    const keyStr = '' + key;\n    if (keyStr === 'nodejs.util.inspect.custom') return blockedSymbolCustomInspect;\n    if (keyStr === 'nodejs.rejection') return blockedSymbolRejection;\n    return originalSymbolFor(keyStr); // everything else passes through\n};\n```\n\nOnly `inspect.custom` and `rejection` are blocked. The following 7 Node.js internal symbols pass through as **real cross-realm symbols**:\n\n- `nodejs.util.promisify.custom`\n- `nodejs.stream.readable`\n- `nodejs.stream.writable`\n- `nodejs.stream.duplex`\n- `nodejs.stream.transform`\n- `nodejs.webstream.isClosedPromise`\n- `nodejs.webstream.controllerErrorFunction`\n\nNote: `bridge.js` `isDangerousCrossRealmSymbol` covers `promisify.custom` on **reads**, but the `Symbol.for` override in setup-sandbox does not block it at the source.\n\n**2. Missing symbol check in bridge write traps** (`bridge.js`):\n\nThe `get` trap (line 1148) and `ownKeys` trap (line 1541) both check `isDangerousCrossRealmSymbol(key)`, but `set` (line 1231), `defineProperty` (line 1427), and `deleteProperty` (line 1493) have **no such check**. Sandbox code can write/define/delete properties with dangerous symbol keys on any non-protected host object.\n\n**3. Incomplete filters in setup-sandbox.js**:\n\n`isDangerousSymbol()`, `Object.getOwnPropertyDescriptors` override, and `Object.assign` override only filter `inspect.custom` and `rejection` — missing `promisify.custom` and all stream/webstream symbols.\n\n## Verified Exploitation: util.promisify Hijack\n\n```js\nconst { VM } = require('vm2');\nconst util = require('util');\n\nconst vm = new VM();\nconst hostFn = function readFile(path, cb) { cb(null, 'real data'); };\nvm.setGlobal('hostFn', hostFn);\n\n// Sandbox writes promisify.custom to host function\nvm.run(`\n  const kPromisify = Symbol.for('nodejs.util.promisify.custom');\n  hostFn[kPromisify] = function(path) {\n    return Promise.resolve('HIJACKED by sandbox');\n  };\n`);\n\n// Host-side: promisified function now returns sandbox-controlled value\nconst asyncRead = util.promisify(hostFn);\nasyncRead('/etc/passwd').then(console.log);\n// Output: \"HIJACKED by sandbox\"\n```\n\n**Additional verified attacks:**\n\n- Writing `nodejs.stream.writable` to a host Readable stream, altering its duck-typing identity\n- `Object.assign` propagates unblocked symbols from sandbox source to host target\n- `Object.defineProperty` with unblocked symbol key succeeds on host objects\n- `delete hostObj[unblocked_symbol]` succeeds, removing host-set symbol properties\n\n## Impact\n\n- **Semantic confusion**: Sandbox controls host `util.promisify` behavior, host stream type checks, and WebStream internals for any non-frozen host object exposed to the sandbox.\n- **Data integrity**: Host code relying on promisified function results gets sandbox-controlled values.\n- **Defense bypass**: Combined with specific host API patterns, sandbox-provided fake streams could bypass host-side input validation.\n\nThis is not a direct RCE — the bridge still wraps sandbox functions crossing the boundary — but it grants the sandbox control over host-side control flow decisions that depend on these symbol-keyed properties.\n\n## Affected Versions\n\n- vm2 \u003c= 3.11.2 (all 3.x versions)\n\n## Environment\n\n- Node.js v24.14.0\n- macOS (Darwin 25.4.0)\n\n## Suggested Fix\n\n1. **`setup-sandbox.js`**: Block all `nodejs.*` prefixed symbols:\n\n```js\nSymbol.for = function (key) {\n    const keyStr = '' + key;\n    if (keyStr.startsWith('nodejs.')) return Symbol(keyStr);\n    return originalSymbolFor(keyStr);\n};\n```\n\n2. **`bridge.js`**: Add check to write traps:\n\n```js\nset(target, key, value, receiver) {\n    if (isDangerousCrossRealmSymbol(key)) throw new VMError(OPNA);\n    // ...\n}\n```\n\n3. **`setup-sandbox.js`**: Sync `isDangerousSymbol`, `Object.getOwnPropertyDescriptors`, `Object.assign` to cover all dangerous symbols.","status":"published","published":"2026-05-29T17:44:32Z","modified":"2026-06-12T19:30:02Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-693"],"updated_at":"2026-11-02T14:40:27.329706Z"}],"nvd":[{"id":"CVE-2026-47135","description":"vm2 is an open source vm/sandbox for Node.js. Prior to version 3.11.4, Symbol.for override in setup-sandbox.js only intercepts 2 of 9 dangerous Node.js cross-realm symbols. Combined with the bridge's set/defineProperty/deleteProperty traps having no isDangerousCrossRealmSymbol key check, sandbox code can obtain real cross-realm symbols, write them to host objects, and control host-side behavior — verified with a full util.promisify hijack chain. This issue has been patched in version 3.11.4.","status":"published","published":"2026-06-12T15:16:28.007Z","modified":"2026-10-26T11:10:00.157Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:N","level":"high","assessor":"secondary"}],"cwes":["CWE-693"],"updated_at":"2026-11-02T14:12:23.846948Z"}],"osv":[{"id":"CVE-2026-47135","title":"vm2: Sandbox escape via unblocked cross-realm Symbol.for keys + missing bridge write-trap symbol checks","description":"vm2 is an open source vm/sandbox for Node.js. Prior to version 3.11.4, Symbol.for override in setup-sandbox.js only intercepts 2 of 9 dangerous Node.js cross-realm symbols. Combined with the bridge's set/defineProperty/deleteProperty traps having no isDangerousCrossRealmSymbol key check, sandbox code can obtain real cross-realm symbols, write them to host objects, and control host-side behavior — verified with a full util.promisify hijack chain. This issue has been patched in version 3.11.4.","status":"published","published":"2026-06-12T14:14:42.022Z","modified":"2026-10-28T02:30:40.567269005Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-693"],"updated_at":"2026-11-02T15:45:06.231145Z"},{"id":"GHSA-m5q2-4fm3-vfqp","title":"vm2 has a sandbox escape via unblocked cross-realm Symbol.for keys + missing bridge write-trap symbol checks","description":"## Summary\n\nvm2 3.11.2 `Symbol.for` override in `setup-sandbox.js` only intercepts 2 of 9 dangerous Node.js cross-realm symbols. Combined with the bridge's `set`/`defineProperty`/`deleteProperty` traps having **no** `isDangerousCrossRealmSymbol` key check, sandbox code can obtain real cross-realm symbols, write them to host objects, and control host-side behavior — verified with a full `util.promisify` hijack chain.\n\n## Root Cause\n\n**1. Incomplete `Symbol.for` override** (`setup-sandbox.js:132-142`):\n\n```js\nSymbol.for = function (key) {\n    const keyStr = '' + key;\n    if (keyStr === 'nodejs.util.inspect.custom') return blockedSymbolCustomInspect;\n    if (keyStr === 'nodejs.rejection') return blockedSymbolRejection;\n    return originalSymbolFor(keyStr); // everything else passes through\n};\n```\n\nOnly `inspect.custom` and `rejection` are blocked. The following 7 Node.js internal symbols pass through as **real cross-realm symbols**:\n\n- `nodejs.util.promisify.custom`\n- `nodejs.stream.readable`\n- `nodejs.stream.writable`\n- `nodejs.stream.duplex`\n- `nodejs.stream.transform`\n- `nodejs.webstream.isClosedPromise`\n- `nodejs.webstream.controllerErrorFunction`\n\nNote: `bridge.js` `isDangerousCrossRealmSymbol` covers `promisify.custom` on **reads**, but the `Symbol.for` override in setup-sandbox does not block it at the source.\n\n**2. Missing symbol check in bridge write traps** (`bridge.js`):\n\nThe `get` trap (line 1148) and `ownKeys` trap (line 1541) both check `isDangerousCrossRealmSymbol(key)`, but `set` (line 1231), `defineProperty` (line 1427), and `deleteProperty` (line 1493) have **no such check**. Sandbox code can write/define/delete properties with dangerous symbol keys on any non-protected host object.\n\n**3. Incomplete filters in setup-sandbox.js**:\n\n`isDangerousSymbol()`, `Object.getOwnPropertyDescriptors` override, and `Object.assign` override only filter `inspect.custom` and `rejection` — missing `promisify.custom` and all stream/webstream symbols.\n\n## Verified Exploitation: util.promisify Hijack\n\n```js\nconst { VM } = require('vm2');\nconst util = require('util');\n\nconst vm = new VM();\nconst hostFn = function readFile(path, cb) { cb(null, 'real data'); };\nvm.setGlobal('hostFn', hostFn);\n\n// Sandbox writes promisify.custom to host function\nvm.run(`\n  const kPromisify = Symbol.for('nodejs.util.promisify.custom');\n  hostFn[kPromisify] = function(path) {\n    return Promise.resolve('HIJACKED by sandbox');\n  };\n`);\n\n// Host-side: promisified function now returns sandbox-controlled value\nconst asyncRead = util.promisify(hostFn);\nasyncRead('/etc/passwd').then(console.log);\n// Output: \"HIJACKED by sandbox\"\n```\n\n**Additional verified attacks:**\n\n- Writing `nodejs.stream.writable` to a host Readable stream, altering its duck-typing identity\n- `Object.assign` propagates unblocked symbols from sandbox source to host target\n- `Object.defineProperty` with unblocked symbol key succeeds on host objects\n- `delete hostObj[unblocked_symbol]` succeeds, removing host-set symbol properties\n\n## Impact\n\n- **Semantic confusion**: Sandbox controls host `util.promisify` behavior, host stream type checks, and WebStream internals for any non-frozen host object exposed to the sandbox.\n- **Data integrity**: Host code relying on promisified function results gets sandbox-controlled values.\n- **Defense bypass**: Combined with specific host API patterns, sandbox-provided fake streams could bypass host-side input validation.\n\nThis is not a direct RCE — the bridge still wraps sandbox functions crossing the boundary — but it grants the sandbox control over host-side control flow decisions that depend on these symbol-keyed properties.\n\n## Affected Versions\n\n- vm2 \u003c= 3.11.2 (all 3.x versions)\n\n## Environment\n\n- Node.js v24.14.0\n- macOS (Darwin 25.4.0)\n\n## Suggested Fix\n\n1. **`setup-sandbox.js`**: Block all `nodejs.*` prefixed symbols:\n\n```js\nSymbol.for = function (key) {\n    const keyStr = '' + key;\n    if (keyStr.startsWith('nodejs.')) return Symbol(keyStr);\n    return originalSymbolFor(keyStr);\n};\n```\n\n2. **`bridge.js`**: Add check to write traps:\n\n```js\nset(target, key, value, receiver) {\n    if (isDangerousCrossRealmSymbol(key)) throw new VMError(OPNA);\n    // ...\n}\n```\n\n3. **`setup-sandbox.js`**: Sync `isDangerousSymbol`, `Object.getOwnPropertyDescriptors`, `Object.assign` to cover all dangerous symbols.","status":"published","published":"2026-05-29T17:44:32Z","modified":"2026-06-12T19:45:09.84080386Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:N","level":"high"}],"cwes":["CWE-693"],"updated_at":"2026-11-02T15:46:51.463834Z"},{"id":"ROOT-APP-NPM-CVE-2026-47135","title":"CVE-2026-47135 in vm2 - Patched by Root","description":"Root has patched CVE-2026-47135 in the vm2 package for Root:npm. Multiple fixed versions available.","status":"published","published":"2026-09-30T06:02:29Z","modified":"2026-09-30T07:15:10.405796893Z","cvss":[{"version":"3.1","score":8.7,"vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:N","level":"high"}],"updated_at":"2026-11-02T15:53:27.649662Z"},{"id":"CLSA-2026-1787860907","title":"TuxCare security update for vm2 (34 CVEs)","description":"TuxCare rebuilt vm2 to address 34 CVEs. Fixed in 3.10.0-tuxcare.1.","status":"published","published":"2026-08-27T20:01:47Z","modified":"2026-10-15T09:48:03.095721636Z","updated_at":"2026-11-02T15:43:47.591214Z"}]}}
