vm2 Sandbox Breakout via __lookupGetter__ Prototype Walk: Overview of CVE-2026-24118

A brief summary of CVE-2026-24118, a critical sandbox breakout in the vm2 Node.js package that allows arbitrary command execution on the host system, along with detection strategies and mitigation guidance.

CVE Analysis

10 min read

ZeroPath CVE Analysis
ZeroPath CVE Analysis

2026-05-04

vm2 Sandbox Breakout via __lookupGetter__ Prototype Walk: Overview of CVE-2026-24118
Experimental AI-Generated Content

This CVE analysis is an experimental publication that is completely AI-generated. The content may contain errors or inaccuracies and is subject to change as more information becomes available. We are continuously refining our process.

If you have feedback, questions, or notice any errors, please reach out to us.

[email protected]

Introduction

A sandbox breakout in the vm2 Node.js package allows any attacker who can submit code to a vm2 sandbox to escape it entirely and execute arbitrary commands on the underlying host system. With over 1.3 million weekly downloads on npm and 898 dependent packages, vm2 is deeply embedded in the Node.js ecosystem, making CVE-2026-24118 a critical concern for any organization running untrusted code through this library.

vm2 is an open source virtual machine and sandbox environment for Node.js, designed to let developers safely execute untrusted JavaScript in an isolated context. It is widely used in low code platforms, serverless function runners, template engines, and developer tooling where user supplied code needs to be evaluated without granting access to the host operating system. Its 4,000 GitHub stars and broad adoption make it one of the most prominent sandboxing solutions in the Node.js world.

Technical Information

Root Cause: Context Bridging and __lookupGetter__

The core of this vulnerability lies in how vm2 bridges objects between the sandbox (guest) context and the host context. The __lookupGetter__ method has a unique behavior in vm2: it switches between the host and sandbox versions when passed across context boundaries. This bridging inconsistency is the entry point for the entire exploit chain.

An attacker operating within the sandbox can exploit this by using Buffer.apply to access the host's apply method. From there, the attacker calls the host version of __lookupGetter__ with Buffer and __proto__ as arguments. This returns the prototype lookup method from the host context rather than the sandbox context.

Exploit Flow

The attack proceeds through the following stages:

  1. Host method access: The attacker uses Buffer.apply from within the sandbox to obtain a reference to the host's apply function.

  2. Prototype chain traversal: By invoking a.apply(g, [Buffer, ['__proto__']]) (where a is the host __lookupGetter__ and g is a crafted object), the attacker walks the prototype chain until reaching the host's Object.prototype.

  3. Constructor acquisition: From Object.prototype, the attacker accesses the .constructor property to obtain the host Object constructor, and then .constructor again to reach the host Function constructor.

  4. Arbitrary code execution: With the host Function constructor in hand, the attacker can create and execute arbitrary functions in the host context. The publicly documented exploit uses this to call require('child_process') and execute system commands.

Bypass of Prior Defenses

An earlier fix attempted to block this class of attack by checking whether the descriptor value name was 'Function' in the bridge logic. This was insufficient. Attackers bypassed it by using Object.getOwnPropertyDescriptor to obtain the constructor property directly, sidestepping the name check entirely.

The fix shipped in version 3.11.0 takes a more comprehensive approach with a two layer defense. It blocks Array species self-return sandbox escapes by ensuring that when the bridge invokes a host function, no host realm array used as context or as an argument can have an attacker controlled constructor property visible to internal V8 processes. This addresses the root cause rather than attempting to filter specific property names.

CVSS Breakdown

MetricValue
Base Score9.8 Critical
Attack VectorNetwork
Attack ComplexityLow
Privileges RequiredNone
User InteractionNone
Confidentiality ImpactHigh
Integrity ImpactHigh
Availability ImpactHigh

The vulnerability is classified under CWE-94 (Improper Control of Generation of Code, or Code Injection) and CWE-693 (Protection Mechanism Failure).

Detection Methods

As of this writing, no formal Sigma, YARA, Suricata, or Snort detection signatures have been published specifically for CVE-2026-24118. Because this vulnerability is an application layer sandbox breakout rather than a network protocol flaw, detection focuses on source code analysis, dependency auditing, and behavioral heuristics rather than traditional network IDS rules.

Version Based Detection (Software Composition Analysis)

The most immediate and reliable detection method is checking whether your environment uses a vulnerable version of the vm2 npm package. Any version prior to 3.11.0 is affected. Security teams should use SCA tools (Snyk, npm audit, Dependabot, Socket, etc.) to flag installations of vm2 below this version. Organizations should also monitor their package-lock.json, yarn.lock, and pnpm-lock.yaml files for the presence of vm2 at versions below 3.11.0. GitHub's Dependabot and npm's built in audit system should surface this advisory automatically via GHSA-grj5-jjm8-h35p.

Heuristic Detection of Exploit Patterns in Sandbox Code

The advisory and the project's ATTACKS.md document provide detailed detection heuristics for identifying exploitation attempts in code being submitted to a vm2 sandbox. These fall into several recognizable pattern families.

The __lookupGetter__ + Buffer.apply Prototype Walk

The exploit's entry point uses a distinctive chain to walk from the sandbox realm to the host realm. Detection should flag any sandbox code that accesses ({}).__lookupGetter__ in combination with Buffer.apply. Any code accessing __lookupGetter__ or __lookupSetter__ on prototype objects, or using Buffer.apply with unexpected arguments, should be treated as highly suspicious.

Array Species Self Return Pattern (Category 18)

The core exploitation primitive is the Array species self return. The ATTACKS.md Category 18 detection rules call out these specific indicators:

  • A pattern of r.constructor = x where x has a Symbol.species property
  • Self referential species declarations like x[Symbol.species] = x
  • Functions used as constructors that return an existing object (e.g., function x() { return r; } serving as a species constructor)
  • Use of ho.assign(r, {constructor: ...}), the Object.assign variant that bypasses the proxy set trap by writing directly on the host object
  • Calls to .map(), .filter(), .slice(), .concat(), or .splice() on arrays that have had their constructor property tampered with, as these all invoke V8's internal ArraySpeciesCreate algorithm
  • ho.entries({}) or Object.entries() used to create host realm arrays destined for species manipulation

Constructor Chain Traversal (Category 1)

Since the ultimate goal is to reach the host Function constructor, monitoring should flag:

  • Any access to .constructor.constructor on any object
  • Patterns resembling Function("return process")() or equivalent dynamic code evaluation strings
  • Access to .constructor inside catch blocks where the error may carry host prototype references

Property Descriptor Value Extraction (Category 15)

Earlier exploit variants used Object.getOwnPropertyDescriptor and Object.entries() to extract the host Function constructor from descriptor values. Detection should flag:

  • Object.getOwnPropertyDescriptor(hostPrototype, 'constructor') extracting the descriptor for the constructor property
  • Chained getOwnPropertyDescriptors calls building deep nesting
  • Object.entries(descriptor) or Object.values(descriptor) used to unpack raw values from descriptors

Runtime Behavioral Monitoring

For environments executing untrusted code in vm2 sandboxes, runtime monitoring can detect exploitation artifacts:

  • Unexpected child process spawning: If a vm2 sandbox escape succeeds, the attacker typically calls require('child_process').execSync(...). Process monitoring (via audit logs, seccomp-bpf, or container level syscall filtering) for child process creation by the Node.js process running the sandbox is a strong post exploitation indicator.
  • Host process object access: EDR or APM tools that instrument Node.js process access can flag unexpected reads of process.mainModule or calls to process.mainModule.require() from within sandbox execution contexts.
  • VMError exceptions: The fix in v3.11.0 throws VMError with messages such as "Unsafe array constructor cannot be neutralized" or "Unsafe non-extensible array passed across bridge" when an active exploitation attempt is detected. Logging and alerting on these specific error messages in production provides a strong signal that an attack was attempted and blocked by the patched version.

Affected Systems and Versions

All versions of the vm2 npm package prior to version 3.11.0 are affected. This includes versions up to and including 3.10.4. The vulnerability was patched in version 3.11.0, which was published on May 1, 2026. The latest available release at the time of writing is version 3.11.2.

Any application or service that uses vm2 to execute user supplied or untrusted JavaScript code is potentially vulnerable. This includes low code platforms, serverless function runners, template engines, and any custom tooling that relies on vm2 for code isolation.

Vendor Security History

The vm2 project has a notable history of sandbox breakout vulnerabilities. The release of version 3.11.0 was a coordinated security release that closed 13 separate advisories, including multiple Remote Code Execution primitives and Denial of Service vectors. This pattern reflects the inherent difficulty of building an airtight in process sandbox using JavaScript's dynamic features.

The maintainers are transparent about these limitations. They explicitly warn users that new bypasses will likely be discovered in the future and that vm2 should not be the only line of defense for untrusted code execution. For completely untrusted code, the vendor recommends stronger isolation alternatives:

SolutionApproachPerformanceTrade offs
isolated vmSeparate V8 isolatesFastIn maintenance mode; requires manual V8 updates
Separate process or Workerchild process or Worker threadsMediumHigher IPC overhead; data must be serialized
Containers or VMsDocker or gVisor or FirecrackerSlowStartup overhead; resource heavy
Managed servicesCloud based code executionVariableNetwork latency; external dependency

References

Detect & fix
what others miss

Security magnifying glass visualization