← Back to PullLight
CVE-2026-44005 CVSS 10.0 Critical npm: 1.3M+ weekly downloads

Prototype Pollution in vm2 Bridge — Sandbox Escape via Host Object Corruption

vm2's Proxy bridge forwards sandbox writes directly to shared host Object.prototype. Attacker-controlled JS escapes the sandbox and corrupts the entire Node.js process.

CVSS 10.0 / 10.0
CWE CWE-1321 → CWE-94
Package vm2 (< 3.11.0)
Published May 1, 2026
1.3M+ weekly npm downloads
Used by AI agent platforms, online code editors, CI tools
Architecture JavaScript Proxy-based sandbox (not V8 Isolate)
// what happened
  • Affected: vm2 < 3.11.0 — Proxy-based JavaScript sandbox library
  • Attack vector: Prototype chain traversal via __lookupGetter__ + Buffer.apply() trick reaches host Object.prototype; otherReflectSet() in BaseHandler forwards sandbox writes directly to shared host objects
  • Impact: Sandbox escape — attacker-controlled JS corrupts host Object.prototype, breaking ALL object behavior for every JavaScript code in the process
  • Fix: Upgrade to vm2 3.11.0+; guard checks added to BaseHandler.get() and otherReflectSet()
  • Structural risk: CVE-2026-44008 and CVE-2026-44009 remain unpatched as of May 2026 — fundamental limitation of Proxy-based sandbox architecture vs. V8 Isolate isolation

Base Score
10.0 Critical
Attack Vector (AV)
Network — attacker-controlled JavaScript code in sandbox
Attack Complexity (AC)
Low — public PoC on GitHub advisory
Privileges Required (PR)
None — arbitrary JS in sandbox is the starting point
User Interaction (UI)
None
Scope (S)
Changed — host process compromised beyond sandbox
Confidentiality (C)
High — full host process access after escape
Integrity (I)
High — arbitrary code execution on host process
Availability (A)
High — host process destabilized or crashed
Vector String
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H

lib/bridge.js — vm2 < 3.11.0 — BaseHandler.apply() forwards to host prototypes vm2 3.10.0 and earlier — no prototype-chain guard in otherReflectSet() // VULNERABLE: vm2 bridge proxy exposes mutable references to host prototypes // Sandbox code can traverse Object.prototype via __lookupGetter__ trick // otherReflectSet() then writes to the host shared prototype object const { VM } = require('vm2'); const vm = new VM(); vm.run(` const g = ({}).__lookupGetter__; const a = Buffer.apply; const p = a.apply(g, [Buffer, ['__proto__']]); const hostObjectProto = p.call(p.call(p.call(p.call(Buffer.of())))); hostObjectProto.vm2EscapeMarker = 'polluted-object-prototype'; `); // host Object.prototype is now corrupted — affects ALL JS in the process console.log({}.vm2EscapeMarker) // → 'polluted-object-prototype' ← sandbox escaped
lib/bridge.js — BaseHandler.get() — returns host-realm __proto__ descriptor vm2 < 3.11.0 — no guard blocking prototype-chain access // VULNERABLE: BaseHandler.get() returns host-side descriptors for // __proto__, constructor — enabling prototype chain traversal from sandbox get(target, key, receiver) { // No check preventing access to __proto__ / constructor on host intrinsics // Sandbox code can reach Object.prototype via these descriptors const desc = Reflect.getOwnPropertyDescriptor(target, key); if (desc) return this._wrap(desc.value, target); // Falls through to get prototype chain — reaches host Object.prototype }
lib/bridge.js — otherReflectSet() — no validation of target prototype vm2 < 3.11.0 — writes forwarded directly to host objects // VULNERABLE: otherReflectSet() blindly forwards sandbox writes to host targets // No check that target is not a host intrinsic prototype (Object.prototype, etc.) otherReflectSet(target, key, value, receiver) { // Sandbox code passes host Object.prototype as target // This corrupts the shared prototype — every object in the host process affected return Reflect.set(target, key, value, receiver); }
lib/bridge.js — vm2 3.11.0 — prototype chain traversal blocked vm2 3.11.0 — guard checks added to BaseHandler // FIX 1: BaseHandler.get() no longer returns host-side __proto__ descriptor get(target, key, receiver) { // Block __proto__ and constructor access on host intrinsics if (key === '__proto__' || key === 'constructor') { // Returns a safe proxy instead of the real host descriptor return undefined; } const desc = Reflect.getOwnPropertyDescriptor(target, key); if (desc) return this._wrap(desc.value, target); return Reflect.get(target, key, receiver); } // FIX 2: otherReflectSet() now validates target is not a host intrinsic prototype otherReflectSet(target, key, value, receiver) { // Guard: reject writes to host intrinsic prototypes if (target === Object.prototype || target === Array.prototype || target === Function.prototype || target === Boolean.prototype || target === Number.prototype || target === String.prototype || target === Symbol.prototype || target === Promise.prototype) { throw new Error('Sandbox write to host prototype blocked'); } return Reflect.set(target, key, value, receiver); }
Recommended upgrade path Fix available in vm2 3.11.0 (May 2026) // Upgrade to vm2 >= 3.11.0 npm install vm2@latest // Or: migrate to V8 Isolate-based isolation (recommended for production) npm install isolated-vm // Use V8 Isolate instead of Proxy-based sandbox — structural defense // against prototype-chain traversal — no bypass exists in this model
// root cause
vm2's Proxy bridge exposes mutable references to host-realm intrinsic prototypes (Object.prototype, Array.prototype, Function.prototype). The BaseHandler.get() method returns host-side __proto__ descriptors, enabling sandbox code to traverse the prototype chain and reach shared host objects. The otherReflectSet() call in BaseHandler.apply() forwards sandbox writes directly to these host objects without validating their identity. Once Object.prototype is polluted from inside the sandbox, every JavaScript code in the host process — including code that never called vm2 — is affected. The prototype chain traversal via __lookupGetter__ + Buffer.apply() is the exploitation route. This is a structural flaw in Proxy-based sandbox design: JavaScript Proxies cannot enforce isolation boundaries when host prototypes are reachable through the prototype chain.

From sandbox code to host process compromise
1
Attacker gets arbitrary JavaScript execution inside a vm2 sandbox (e.g., through a webhook, AI prompt evaluation, or online code editor).
2
Sandbox code uses ({}).__lookupGetter__ to get a reference to the __lookupGetter__ function via the Proxy bridge — this function exists in the sandbox but its internal behavior reaches host realms.
3
The Buffer.apply() trick is used to invoke __lookupGetter__ with ['__proto__'] as argument, navigating the prototype chain to reach the host's Object.prototype.
4
otherReflectSet() writes to the host Object.prototype from inside the sandbox — this is the sandbox escape. The host's Object.prototype is now polluted with attacker-controlled properties.
5
Any subsequent code in the host process — including code that has nothing to do with vm2 — now operates on a corrupted prototype chain. All objects inherit the attacker's properties.
6
Attacker can use the prototype pollution to escalate to arbitrary code execution on the host process (e.g., by polluting Object.prototype.constructor to point to a different constructor, or by exploiting other globals that depend on prototype integrity).
vm2 is embedded in production systems across the web
  • AI agent platforms — evaluate user prompts as JavaScript code using vm2 sandbox; attacker-controlled prompts can escape the sandbox and compromise the agent infrastructure
  • Online code editors — let users run JavaScript snippets; prototype pollution from one user affects the entire editor session and potentially other users' code
  • CI automation tools — use vm2 to run arbitrary user-provided scripts in isolated contexts; sandbox escape gives access to CI secrets and build environments
  • Webhook processing systems — evaluate dynamic expressions or rules defined by external users; prototype pollution corrupts the webhook processor's internal state
  • Plugin / extension systems — many Node.js plugins embed user-supplied JavaScript in vm2 sandboxes for extensibility
Host Process Compromise
Sandbox escape breaks the isolation boundary — attacker code runs with the full privileges of the Node.js process.
Global Prototype Corruption
Polluting Object.prototype affects ALL JavaScript code in the process — including code that never used vm2.
Structural Sandbox Flaw
CVE-2026-44008 and CVE-2026-44009 remain unpatched — Proxy-based isolation cannot be made safe against prototype-chain traversal.
1.3M+ Weekly Downloads
vm2's npm footprint means millions of deployments may be affected — and many won't update quickly.

src/services/sandbox.js — Node.js sandbox evaluation with vm2 // Sandbox service using vm2 to run untrusted user code const { VM } = require('vm2'); function evaluateUserCode(userCode, context) { const vm = new VM({ eval: false, wasm: false, fixedAsync: true }); // Run user-controlled JavaScript in sandbox // VULNERABLE: vm2's Proxy bridge doesn't protect host prototypes // Prototype pollution via __lookupGetter__ + otherReflectSet() // allows sandbox code to corrupt Object.prototype on the host return vm.run(userCode); } module.exports = { evaluateUserCode }; // The evaluateUserCode function is the entry point PullLight would flag
PullLight synthetic fix — migrate to V8 Isolate-based isolation // PullLight would flag: vm2's Proxy-based sandbox has no structural // defense against prototype-chain traversal — every untrusted user input // reaching vm2.VM.run() is a critical sandbox escape vulnerability. // Fix: replace vm2 with isolated-vm (V8 Isolate-based) or Docker-based sandbox const ivm = require('isolated-vm'); async function evaluateUserCode(userCode, context) { const isolate = await ivm.createIsolate({ memory: 128 }); const snapshot = await isolate.compileScript(userCode); const result = await snapshot.run(isolate); isolate.dispose(); return result; } // V8 Isolate provides structural isolation — prototype chain traversal // cannot reach the host because there's no shared prototype chain. // This is the only truly safe approach for untrusted code execution.
🔴 PullLight — Critical Finding
[CRITICAL] Sandbox Escape via Prototype Pollution — src/services/sandbox.js

Sandbox isolation relies on JavaScript Proxy boundary — prototype pollution in any sandboxed code can break out by mutating shared host Object.prototype. BaseHandler.apply() forwards sandbox writes to host-realm prototypes via otherReflectSet(). Any code path where untrusted user input reaches vm2.VM.run() without strict Object.prototype freezing is a critical finding — this bypasses the entire sandbox model.

This is CWE-1321 (Prototype Pollution)CWE-94 (Code Injection) with a CVSS 10.0 rating. It affects every application using vm2 to run untrusted JavaScript.
→ Fix: Use V8 Isolate-based isolation (isolated-vm) or OS-level sandboxing (Docker) instead of vm2's Proxy-based sandbox, which has no structural defense against prototype-chain traversal. Upgrade to vm2 3.11.0+ at minimum, but note that CVE-2026-44008 and CVE-2026-44009 remain unpatched — Proxy-based sandboxes are architecturally unsafe for untrusted code.

Five reasons this vulnerability hides from line-by-line review

  • Proxy bridges look like legitimate architecture. vm2's Proxy-based bridge is a documented design pattern — there's no obvious "bug" in the code structure. The vulnerability is in the interaction between Proxy forwarding and the JavaScript prototype chain, not in a syntactic error.
  • The exploit uses legitimate JavaScript. __lookupGetter__, Buffer.apply() — these are standard JavaScript APIs. A reviewer seeing this code in isolation would not recognize it as an attack vector.
  • Prototype pollution is a frontend-focused bug class. Most developers associate prototype pollution with XSS and client-side attacks. The idea that prototype pollution from a sandbox can affect the host process is counterintuitive and rarely modeled.
  • The bridge proxy is in framework internals. The vulnerable code is in lib/bridge.js — a framework internals file. Reviewers trust the sandbox library's code more than their own application's code.
  • Structural flaw, not a missing check. Even patching otherReflectSet() leaves CVE-2026-44008 and CVE-2026-44009 unpatched — the Proxy-based architecture itself is the problem, not any individual missing guard. Human reviewers can't see this without knowing the full disclosure history.

What makes this a PullLight-specialized catch

  • Cross-prototype understanding: PullLight models how Proxy forwarding interacts with JavaScript's prototype chain — it can trace the path from __lookupGetter__ through the bridge to Object.prototype.
  • Architecture-level analysis: PullLight recognizes that Proxy-based sandboxes are architecturally unsafe for untrusted code — it flags the use of vm2 in contexts where untrusted input reaches vm2.VM.run() as a structural concern, not just a missing input validation.
  • CWE chain modeling: CWE-1321 (Prototype Pollution) → CWE-94 (Code Injection) → CWE-20 (Improper Input Validation). PullLight models the full attack chain, not just individual CVEs.
  • Remediation precision: PullLight recommends isolated-vm (V8 Isolate) over vm2 — not just "upgrade to 3.11.0" — because the structural fix is different from the patch for this specific CVE.

May 1, 2026 Security advisory published (GHSA-vwrp-x96c-mhwq). vm2 3.11.0 released with patch.
May 2026 10+ CVEs disclosed in vm2 as part of coordinated disclosure wave — CVE-2026-44005 through CVE-2026-44015.
May 2026 CVE-2026-44005 added to NVD. CVSS 10.0 confirmed.
May 2026 CVE-2026-44008 and CVE-2026-44009 remain unpatched — structural Proxy-sandbox limitations confirmed by maintainer.
Jun 2026 PullLight documents CVE-2026-44005 as 8th CVE case study — demonstrating prototype pollution and sandbox escape detection in PR review.

Don't let sandbox escapes reach your PRs

More CVE Case Studies