GHSA-3vgf-8m4q-q4qr: Critical severity npm/vm2 vulnerability
Summary
vm2's current host-intrinsic prototype protection is incomplete. The fix for GHSA-vwrp-x96c-mhwq blocks sandbox writes into classic host intrinsics such as Object.prototype, Array.prototype, and Function.prototype, but current head still lets sandbox code in a default VM reach and mutate host Uint8Array.prototype, %TypedArray%.prototype, and ArrayBuffer.prototype.
After VM.run() returns, normal host typed-array and ArrayBuffer objects observe attacker-controlled properties and methods installed by the sandbox.
Technical Details
The existing mitigation relies on protectedHostObjects in lib/bridge.js. That set is populated from otherGlobalPrototypes, which is built from a fixed inventory of classic globals:
js const globalsList = [ 'Number', 'String', 'Boolean', 'Date', 'RegExp', 'Map', 'WeakMap', 'Set', 'WeakSet', 'Promise', 'Function' ];
The inventory omits typed-array and ArrayBuffer intrinsics. Sandbox code can still reuse the host-prototype walking primitive from the prior public advisory:
js const lookupGetter = ({}).lookupGetter; const apply = Buffer.apply; const protoGetter = apply.apply(lookupGetter, [Buffer, ['proto']]); const hostBuffer = Buffer.from([1]);
const hostBufferPrototype = protoGetter.call(hostBuffer); const hostUint8ArrayPrototype = protoGetter.call(hostBufferPrototype); const hostTypedArrayPrototype = protoGetter.call(hostUint8ArrayPrototype); const hostArrayBufferPrototype = protoGetter.call(hostBuffer.buffer);
Those objects are not sandbox-local. Inside the sandbox, hostUint8ArrayPrototype === Uint8Array.prototype, hostTypedArrayPrototype === Object.getPrototypeOf(Uint8Array.prototype), and hostArrayBufferPrototype === ArrayBuffer.prototype are all false. Because the objects are not in protectedHostObjects, bridge defineProperty writes are forwarded into the real host objects.
This is the same guard-coverage boundary as the previous host-intrinsic prototype pollution fix, but with a missing intrinsic family. The bridge already has the correct enforcement shape; the protected inventory is too narrow.
PoC
PoV: poc/pov-host-typedarray-arraybuffer-prototype-pollution.js
Current-head output:
json { "status": "completed", "vulnerable": true, "node": "v25.8.0", "v8": "14.1.146.11-node.20", "control": { "defineResult": true, "sandboxReadsBack": "sandbox-only", "hostControlAfter": null }, "exploit": { "hostUint8IsSandboxUint8": false, "hostTypedArrayIsSandboxTypedArray": false, "hostArrayBufferIsSandboxArrayBuffer": false, "defineUint8": true, "defineTypedArray": true, "defineArrayBuffer": true, "defineMethod": true }, "hostEffect": { "uint8Marker": "polluted-host-uint8array-prototype", "typedArrayMarker": "polluted-host-typedarray-prototype", "arrayBufferMarker": "polluted-host-arraybuffer-prototype", "methodReturn": "sandbox-method-reached-host-uint8array" } }
The control proves sandbox-local Uint8Array.prototype writes remain sandbox-local. The exploit path reaches host prototypes through the bridge and causes host-created objects to observe sandbox-installed properties after VM.run() returns.
Impact
This is a sandbox boundary violation and host intrinsic prototype pollution. An attacker who can run JavaScript in a default vm2 VM can mutate shared host typed-array and ArrayBuffer behavior without NodeVM, require, wildcard builtins, nesting: true, or any host-provided typed-array object.
The supplied PoV demonstrates integrity and availability impact by installing markers and a method on host prototypes:
- Uint8Array.prototype - %TypedArray%.prototype, which affects typed-array families through the shared typed-array prototype chain - ArrayBuffer.prototype
The PoV does not claim direct host command execution or direct confidentiality impact. It is local-only and restores touched descriptors before exit.
Suggested Fix
Extend the protected host-object inventory and identity/prototype mappings to typed-array and binary-data intrinsics, including at least:
- %TypedArray%.prototype - ArrayBuffer.prototype - SharedArrayBuffer.prototype when present - DataView.prototype - all concrete typed-array prototypes present in the runtime, including Uint8Array.prototype, Uint8ClampedArray.prototype, Int8Array.prototype, Uint16Array.prototype, Int16Array.prototype, Uint32Array.prototype, Int32Array.prototype, Float16Array.prototype when present, Float32Array.prototype, Float64Array.prototype, BigInt64Array.prototype, and BigUint64Array.prototype
A temporary patched-control that added this intrinsic family to thisGlobalPrototypes caused the same PoV's Reflect.defineProperty() calls to throw VMError: Operation not allowed on contextified object, and no host markers were installed.
The fix should cover the same host mutation traps used by the existing host-intrinsic protection: set, defineProperty, deleteProperty, and preventExtensions. It should not special-case Buffer; Buffer is only one way to reach the omitted host prototypes.
Affected Package/Versions
Confirmed on current head 7a1f5100b96f48d34e0fe104ab37c0acc5944f92 / v3.11.5 with Node v25.8.0 / V8 14.1.146.11-node.20.
Confirmed affected after the prior patch: npm:vm2 >= 3.11.0, <= 3.11.5.
Local sweep also reproduces on v3.10.0 through v3.10.5, but that range overlaps the already-published GHSA-vwrp-x96c-mhwq range. v3.9.0 through v3.9.2 did not produce host-visible pollution in the same local test.
Why This Is Not Intended Behavior
vm2 documents VM as a sandbox for untrusted code without require, with only JavaScript built-ins and Node's Buffer available by default. The known escape hatches do not explain this issue:
- require.builtin: [''] is a NodeVM configuration; this PoV uses default VM. - nesting: true is not enabled or used. - The README timeout caveat covers host code operating on objects returned from the sandbox; this PoV mutates host intrinsics and later affects ordinary host-created typed arrays and ArrayBuffers.
The project's own attack notes state that host-realm intrinsic prototypes should be protected from sandbox writes, while non-intrinsic host objects may remain mutable when intentionally exposed. Typed-array and ArrayBuffer prototypes are host intrinsics, not embedder-owned application objects.
Buffer availability explains how the PoV reaches the host prototype chain, but it does not authorize mutation of unrelated host-realm intrinsics. The host effects are observed on new host-created typed arrays and ArrayBuffers after VM.run() returns; the host is not invoking an object returned from the sandbox.
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.8 - Configuration
Extend the protected host-object inventory and identity/prototype mappings to include ArrayBuffer.prototype, DataView.prototype, SharedArrayBuffer.prototype when present, %TypedArray%.prototype, and all concrete typed-array prototypes including Uint8Array.prototype; apply the existing host-intrinsic protection to set, defineProperty, deleteProperty, and preventExtensions operations.
vm2 lib/bridge.js protectedHostObjects / thisGlobalPrototypes = Include typed-array and binary-data host intrinsics
Event History
Frequently Asked Questions
Is a non-default vm2 configuration required for exploitation?
No. The issue is reachable from sandbox code in a default VM configuration.
What capability does an attacker need?
An attacker needs the ability to execute JavaScript inside a vm2 sandbox. No privileges or user interaction are indicated by the supplied severity vector.
What is the practical impact after the sandbox execution completes?
The sandbox can install attacker-controlled properties and methods on host Uint8Array.prototype, %TypedArray%.prototype, and ArrayBuffer.prototype. Normal host typed-array and ArrayBuffer objects can then observe those modifications after VM.run() returns.