CVE-2026-92962
vm2 before 3.11.4 Defense Invariant Violation via setup-sandbox.js
vm2 is a sandbox for running untrusted JavaScript. In vm2 versions up to and including 3.11.3, the defaultSandboxPrepareStackTrace function in lib/setup-sandbox.js builds its output array using prototype-walking index assignment (lines[lines.length] = value) rather than a prototype-bypassing define-property primitive. Because this bridge-internal array is allocated in the sandbox realm, code inside the sandbox can install an accessor on Array.prototype for the relevant index; the accessor is then invoked whenever the sandbox reads error.stack (or otherwise triggers Error.prepareStackTrace), allowing sandbox code to observe and intercept each stack-trace line written by the bridge. The same pattern is used in the error-handling (catch) branch. The values written are formatted strings only, so the practical impact is limited to an information side channel and a violation of vm2's bridge-container defense invariant rather than a sandbox escape; the vendor rates the issue Low. The issue is fixed in vm2 3.11.4, which installs each entry as an own data property via Reflect.defineProperty.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- High
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- None
Exploit likelihood
What each source reports
CVE List Score used
2.1
vm2 before 3.11.4 Defense Invariant Violation via setup-sandbox.js
vm2 is a sandbox for running untrusted JavaScript. In vm2 versions up to and including 3.11.3, the defaultSandboxPrepareStackTrace function in lib/setup-sandbox.js builds its output array using prototype-walking index assignment (lines[lines.length] = value) rather than a prototype-bypassing define-property primitive. Because this bridge-internal array is allocated in the sandbox realm, code inside the sandbox can install an accessor on Array.prototype for the relevant index; the accessor is then invoked whenever the sandbox reads error.stack (or otherwise triggers Error.prepareStackTrace), allowing sandbox code to observe and intercept each stack-trace line written by the bridge. The same pattern is used in the error-handling (catch) branch. The values written are formatted strings only, so the practical impact is limited to an information side channel and a violation of vm2's bridge-container defense invariant rather than a sandbox escape; the vendor rates the issue Low. The issue is fixed in vm2 3.11.4, which installs each entry as an own data property via Reflect.defineProperty.
- Record
CVE-2026-92962- CVSS 4.0
- 2.1 Low
CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N - Weakness
- CWE-693
- Updated
- 2026-11-02
EUVDEUVD-2026-81612
2.1
vm2 is a sandbox for running untrusted JavaScript. In vm2 versions up to and including 3.11.3, the defaultSandboxPrepareStackTrace function in lib/setup-sandbox.js builds its output array using prototype-walking index assignment (lines[lines.length] = value) rather than a prototype-bypassing define-property primitive. Because this bridge-internal array is allocated in the sandbox realm, code inside the sandbox can install an accessor on Array.prototype for the relevant index; the accessor is then invoked whenever the sandbox reads error.stack (or otherwise triggers Error.prepareStackTrace), allowing sandbox code to observe and intercept each stack-trace line written by the bridge. The same pattern is used in the error-handling (catch) branch. The values written are formatted strings only, so the practical impact is limited to an information side channel and a violation of vm2's bridge-container defense invariant rather than a sandbox escape; the vendor rates the issue Low. The issue is fixed in vm2 3.11.4, which installs each entry as an own data property via Reflect.defineProperty.
- Record
EUVD-2026-81612- CVSS 4.0
- 2.1 Low
CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-2c9f-f88g-g76p
2.1
vm2 is a sandbox for running untrusted JavaScript. In vm2 versions up to and including 3.11.3,...
vm2 is a sandbox for running untrusted JavaScript. In vm2 versions up to and including 3.11.3, the defaultSandboxPrepareStackTrace function in lib/setup-sandbox.js builds its output array using prototype-walking index assignment (lines[lines.length] = value) rather than a prototype-bypassing define-property primitive. Because this bridge-internal array is allocated in the sandbox realm, code inside the sandbox can install an accessor on Array.prototype for the relevant index; the accessor is then invoked whenever the sandbox reads error.stack (or otherwise triggers Error.prepareStackTrace), allowing sandbox code to observe and intercept each stack-trace line written by the bridge. The same pattern is used in the error-handling (catch) branch. The values written are formatted strings only, so the practical impact is limited to an information side channel and a violation of vm2's bridge-container defense invariant rather than a sandbox escape; the vendor rates the issue Low. The issue is fixed in vm2 3.11.4, which installs each entry as an own data property via Reflect.defineProperty.
- Record
GHSA-2c9f-f88g-g76p- CVSS 4.0
- 2.1 Low
CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - Weakness
- CWE-693
- Updated
- 2026-11-02
GitHub AdvisoriesGHSA-q3fm-4wcw-g57x
2.1
vm2 setup-sandbox.js violates Defense Invariant #11 in stack-trace formatter
Summary
defaultSandboxPrepareStackTrace in lib/setup-sandbox.js (lines 605, 607) appends to a fresh sandbox-realm lines = [] via lines[lines.length] = value. This is the exact invariant-violating pattern that GHSA-9qj6-qjgg-37qq (commit ca195f0, 2026-05-01) just patched in neutralizeArraySpeciesBatch and codified as Defense Invariant #11 ("Bridge-internal containers must not invoke sandbox code"). A sandbox-installed Array.prototype[N] setter fires during the bridge's safe-default stack-trace formatting and observes / intercepts each appended line.
Details
The post-9qj6 audit note in docs/ATTACKS.md (line 2111) states:
Equivalent pattern elsewhere in the bridge: audited; thisFromOtherArguments, otherFromThisArguments, and every other index-write site already use thisReflectDefineProperty or otherReflectDefineProperty. neutralizeArraySpeciesBatch was the lone outlier.
The audit is scoped to lib/bridge.js. lib/setup-sandbox.js was not covered. defaultSandboxPrepareStackTrace (added under post-#563 hardening for GHSA-v27g) constructs a sandbox-realm [header] array and appends each frame via the prototype-walking index assignment:
// lib/setup-sandbox.js, lines 601-610
const lines = [header];
for (let i = 0; i < callSites.length; i++) {
try {
lines[lines.length] = ' at ' + callSites[i];
} catch (e) {
lines[lines.length] = ' at <error formatting frame>';
}
}
return lines.join('\n');
This function runs every time sandbox code reads error.stack (or any path that triggers Error.prepareStackTrace). At the time it runs, user code has already had the opportunity to install a setter on Array.prototype[N]. Because lines starts at length 1, the first iteration writes index 1; if lines[1] has no own data property, V8 walks the prototype chain and invokes the sandbox-controlled setter.
The currently-assigned value is the string ' at ' + callSites[i] (the wrapped CallSite class's safe toString() returns 'CallSite {}'), which limits the immediate impact to a side channel, not an RCE pivot. The concern is structural rather than exploit-today:
- The just-codified Defense Invariant #11 explicitly requires that any list, set, or map allocated for the bridge's exclusive use must read and write through identity-stable, prototype-bypassing primitives. This site does not.
- The
catchbranch at line 607 also uses the same pattern, so a sandbox getter that throws oncallSites[i]access still routes its retry write through the prototype chain. - A future change that makes the appended slot value an object holding a host-realm reference (for example, an enriched frame record) would re-introduce the exact GHSA-9qj6 attack shape against this codepath.
The fix is mechanical and mirrors the GHSA-9qj6 patch: install entries via localReflectDefineProperty so each appended slot is an own data property and the prototype-chain setter is bypassed.
// Suggested patch (sketch)
let linesLen = 1;
function append(s) {
localReflectDefineProperty(lines, linesLen, {
__proto__: null,
value: s,
writable: true,
enumerable: true,
configurable: true,
});
linesLen++;
}
for (let i = 0; i < callSites.length; i++) {
try {
append(' at ' + callSites[i]);
} catch (e) {
append(' at <error formatting frame>');
}
}
The same pattern at callSiteGetters[callSiteGetters.length] = {...} (line 649) runs only at sandbox setup, before user code can install setters, so it is safe today. Converting it for symmetry would be cheap and forward-compatible.
PoC
vm2 v3.11.2, Node v24.
const { VM } = require('vm2');
const result = new VM().run(`
var observed = { setterFired: false, capturedValue: null, indexFired: null };
Object.defineProperty(Array.prototype, 1, {
configurable: true,
set(value) {
observed.setterFired = true;
…
- Record
GHSA-q3fm-4wcw-g57x- CVSS 4.0
- 2.1 Low
CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N - Weakness
- CWE-693
- Updated
- 2026-11-02
NVD
2.1
vm2 is a sandbox for running untrusted JavaScript. In vm2 versions up to and including 3.11.3, the defaultSandboxPrepareStackTrace function in lib/setup-sandbox.js builds its output array using prototype-walking index assignment (lines[lines.length] = value) rather than a prototype-bypassing define-property primitive. Because this bridge-internal array is allocated in the sandbox realm, code inside the sandbox can install an accessor on Array.prototype for the relevant index; the accessor is then invoked whenever the sandbox reads error.stack (or otherwise triggers Error.prepareStackTrace), allowing sandbox code to observe and intercept each stack-trace line written by the bridge. The same pattern is used in the error-handling (catch) branch. The values written are formatted strings only, so the practical impact is limited to an information side channel and a violation of vm2's bridge-container defense invariant rather than a sandbox escape; the vendor rates the issue Low. The issue is fixed in vm2 3.11.4, which installs each entry as an own data property via Reflect.defineProperty.
- Record
CVE-2026-92962- CVSS 4.0
- 2.1 Low
CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - Weakness
- CWE-693
- Updated
- 2026-11-02
OSV
2.1
vm2 before 3.11.4 Defense Invariant Violation via setup-sandbox.js
vm2 is a sandbox for running untrusted JavaScript. In vm2 versions up to and including 3.11.3, the defaultSandboxPrepareStackTrace function in lib/setup-sandbox.js builds its output array using prototype-walking index assignment (lines[lines.length] = value) rather than a prototype-bypassing define-property primitive. Because this bridge-internal array is allocated in the sandbox realm, code inside the sandbox can install an accessor on Array.prototype for the relevant index; the accessor is then invoked whenever the sandbox reads error.stack (or otherwise triggers Error.prepareStackTrace), allowing sandbox code to observe and intercept each stack-trace line written by the bridge. The same pattern is used in the error-handling (catch) branch. The values written are formatted strings only, so the practical impact is limited to an information side channel and a violation of vm2's bridge-container defense invariant rather than a sandbox escape; the vendor rates the issue Low. The issue is fixed in vm2 3.11.4, which installs each entry as an own data property via Reflect.defineProperty.
- Record
CVE-2026-92962- CVSS 4.0
- 2.1 Low
CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N - Weakness
- CWE-693
- Updated
- 2026-11-02
OSVGHSA-q3fm-4wcw-g57x
2.1
vm2 setup-sandbox.js violates Defense Invariant #11 in stack-trace formatter
Summary
defaultSandboxPrepareStackTrace in lib/setup-sandbox.js (lines 605, 607) appends to a fresh sandbox-realm lines = [] via lines[lines.length] = value. This is the exact invariant-violating pattern that GHSA-9qj6-qjgg-37qq (commit ca195f0, 2026-05-01) just patched in neutralizeArraySpeciesBatch and codified as Defense Invariant #11 ("Bridge-internal containers must not invoke sandbox code"). A sandbox-installed Array.prototype[N] setter fires during the bridge's safe-default stack-trace formatting and observes / intercepts each appended line.
Details
The post-9qj6 audit note in docs/ATTACKS.md (line 2111) states:
Equivalent pattern elsewhere in the bridge: audited; thisFromOtherArguments, otherFromThisArguments, and every other index-write site already use thisReflectDefineProperty or otherReflectDefineProperty. neutralizeArraySpeciesBatch was the lone outlier.
The audit is scoped to lib/bridge.js. lib/setup-sandbox.js was not covered. defaultSandboxPrepareStackTrace (added under post-#563 hardening for GHSA-v27g) constructs a sandbox-realm [header] array and appends each frame via the prototype-walking index assignment:
// lib/setup-sandbox.js, lines 601-610
const lines = [header];
for (let i = 0; i < callSites.length; i++) {
try {
lines[lines.length] = ' at ' + callSites[i];
} catch (e) {
lines[lines.length] = ' at <error formatting frame>';
}
}
return lines.join('\n');
This function runs every time sandbox code reads error.stack (or any path that triggers Error.prepareStackTrace). At the time it runs, user code has already had the opportunity to install a setter on Array.prototype[N]. Because lines starts at length 1, the first iteration writes index 1; if lines[1] has no own data property, V8 walks the prototype chain and invokes the sandbox-controlled setter.
The currently-assigned value is the string ' at ' + callSites[i] (the wrapped CallSite class's safe toString() returns 'CallSite {}'), which limits the immediate impact to a side channel, not an RCE pivot. The concern is structural rather than exploit-today:
- The just-codified Defense Invariant #11 explicitly requires that any list, set, or map allocated for the bridge's exclusive use must read and write through identity-stable, prototype-bypassing primitives. This site does not.
- The
catchbranch at line 607 also uses the same pattern, so a sandbox getter that throws oncallSites[i]access still routes its retry write through the prototype chain. - A future change that makes the appended slot value an object holding a host-realm reference (for example, an enriched frame record) would re-introduce the exact GHSA-9qj6 attack shape against this codepath.
The fix is mechanical and mirrors the GHSA-9qj6 patch: install entries via localReflectDefineProperty so each appended slot is an own data property and the prototype-chain setter is bypassed.
// Suggested patch (sketch)
let linesLen = 1;
function append(s) {
localReflectDefineProperty(lines, linesLen, {
__proto__: null,
value: s,
writable: true,
enumerable: true,
configurable: true,
});
linesLen++;
}
for (let i = 0; i < callSites.length; i++) {
try {
append(' at ' + callSites[i]);
} catch (e) {
append(' at <error formatting frame>');
}
}
The same pattern at callSiteGetters[callSiteGetters.length] = {...} (line 649) runs only at sandbox setup, before user code can install setters, so it is safe today. Converting it for symmetry would be cheap and forward-compatible.
PoC
vm2 v3.11.2, Node v24.
const { VM } = require('vm2');
const result = new VM().run(`
var observed = { setterFired: false, capturedValue: null, indexFired: null };
Object.defineProperty(Array.prototype, 1, {
configurable: true,
set(value) {
observed.setterFired = true;
…
- Record
GHSA-q3fm-4wcw-g57x- CVSS 4.0
- 2.1 Low
CVSS:4.0/AV:L/AC:H/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:L/SA:N - Weakness
- CWE-693
- Updated
- 2026-11-02
OSVCLSA-2026-1787860907 No score
TuxCare security update for vm2 (34 CVEs)
TuxCare rebuilt vm2 to address 34 CVEs. Fixed in 3.10.0-tuxcare.1.
- Record
CLSA-2026-1787860907- Updated
- 2026-11-02
Affected products
Affected packages
History
| Recorded | Changed | Sources |
|---|---|---|
| facts | ||
| affected, aliases, references | ||
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-92962 is a low vulnerability (CVSS 2.1) in Patriksimek Vm2, published May 29, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.15%, the 4th percentile. Fixed in vm2 3.11.4.
References
github.com/patriksimek/vm2/security/advisories/GHSA-q3fm-4wcw-g57x
vulncheck.com/advisories/vm2-before-3.11.4-defense-invariant-violation-via-setup-sandbox-js
nvd.nist.gov/vuln/detail/CVE-2026-92962
github.com/advisories/GHSA-2c9f-f88g-g76p
github.com/patriksimek/vm2/commit/ad31adc1fc4a2c163f2f8c11ab4af206074528fd
github.com/patriksimek/vm2/releases/tag/v3.11.4
github.com/advisories/GHSA-q3fm-4wcw-g57x
github.com/CVEProject/cvelistV5/tree/main/cves/2026/92xxx/CVE-2026-92962.json
github.com/patriksimek/vm2
Left out here: the rest of 2 source descriptions. The JSON record has them.