1. Executive summary
A sandbox escape in the vm2 Node.js library allows code running inside a NodeVM instance to reach the host __proto__ getter and execute arbitrary commands in the host process context. The flaw is reachable through console._stdout and console._stderr under the default console: 'inherit' setting, and it defeats --disallow-code-generation-from-strings, a control some teams rely on to block the standard vm2 escape. The GitHub Advisory Database records a CVSS v4 base score of 10.0 (Critical) with network attack vector, low complexity, no privileges and no user interaction. No CISA KEV entry, EPSS score, or vendor patch is available in the source material, and the advisory states the underlying __proto__ access has been reported repeatedly without being fixed. EMEA financial services exposure is concentrated wherever vm2 is used to run untrusted or third-party JavaScript: plugin and rule engines, customer-supplied scripting, low-code workflow nodes, and CI/CD tooling that evaluates external code.
2. Regulatory framing
No specific DORA/NIS2 article is directly engaged by this item. The source describes a library vulnerability with a proof of concept; it does not describe an incident at a named entity, a supply chain compromise, or a third-party provider failure. DORA Art. 17, 18 and 19 attach to the management, classification and reporting of an actual ICT-related incident, and none is evidenced here. NIS2 Art. 21(2)(d) and Art. 23 attach to supply chain security measures and incident reporting respectively, and neither is triggered by a published advisory alone. If a client confirms exploitation of this flaw in production, DORA Art. 17 and 18 become engaged at that point, and the classification and reporting clock under Art. 19 starts from incident detection, not from this advisory.
3. Technical analysis & attack chain
The vulnerability is a sandbox escape in vm2's NodeVM. The source states the host __proto__ getter/setter has been obtainable across multiple prior reports (GHSA-vwrp-x96c-mhwq, GHSA-v6mx-mf47-r5wg, GHSA-grj5-jjm8-h35p, GHSA-47x8-96vw-5wg6) and that this access path was never patched. The advisory is published as GHSA-88hf-g992-jg85; a duplicate advisory, GHSA-gg6f-mhqm-f7gp, was withdrawn and points to the same issue. The duplicate's original description states the flaw affects vm2 before 3.11.8.
Confirmed steps from the proof of concept:
- Code executes inside a
NodeVMinstance created with default options, meaningconsole: 'inherit'is in effect. - The sandboxed code obtains the host
__proto__getter. The PoC does this withBuffer.call.call(__lookupGetter__, 67, '__proto__'), assigning the result togP. The value 67 is the property key index used to reach__proto__through__lookupGetter__. - The code walks the prototype chain of
console._stdout(orconsole._stderr) usinggP.call(p)whilep.pipeis truthy. The chain is_stdout/_stderrtoWriteStream(TTY only) toSockettoDuplextoReadabletoStreamtoEventEmitter. - Because
processis anEventEmitter, the code overwritesEventEmitter.prototype.emitwith an attacker-supplied function. - The attacker triggers a
processevent such asexitorunhandledRejection. The overwrittenemitruns withthisbound toprocess. - Inside that function,
this.getBuiltinModule("child_process").execSync("sh", {stdio: "inherit"})spawns an interactive shell in the host process context.
The escape bypasses --disallow-code-generation-from-strings, which blocks the usual route of obtaining the host Function constructor. That matters for defenders who treat the flag as a compensating control for vm2: it does not stop this path.
The source gives no information on how an attacker would first get code into a NodeVM instance in a real deployment. That depends entirely on the host application. Where the application runs untrusted user or tenant JavaScript, the input is direct. Where it runs only first-party code, the flaw is not reachable without a separate injection primitive. The source also gives no exploitation in the wild, no malware, no persistence mechanism, no command-and-control, and no observed impact. The PoC is a local demonstration, not a campaign report.
The CVSS v4 vector in the source is network attack vector, low complexity, no attack requirements, no privileges required, no user interaction, with high confidentiality, integrity and availability impact to both the vulnerable and subsequent systems. That vector assumes the vulnerable component is reachable over a network, which is true of a hosted service that evaluates untrusted JavaScript and not true of a library embedded in a desktop tool. Treat the 10.0 as the upper bound for an internet-facing evaluator, not as the score for every deployment.
4. Mitigation & containment
P1 (within 24 hours)
- Inventory every application, service and CI job that depends on vm2. Run
npm ls vm2andyarn why vm2across repositories, and check lockfiles (package-lock.json,yarn.lock,pnpm-lock.yaml) for transitive copies pulled in by other packages. Transitive dependencies are the common blind spot here. - For any instance that evaluates untrusted or third-party JavaScript, treat the sandbox as broken. Move that evaluation into a separate process or container with no host filesystem mount, no network egress beyond what the task requires, and a non-privileged user. Do not rely on vm2's isolation boundary for anything that matters.
- If the application does not need
console: 'inherit', setconsole: 'off'or supply a custom console object. The source namesconsole: 'inherit'as the default and the vector for reachingconsole._stdout/console._stderr. This reduces the exposed surface but the source does not claim it closes the flaw, so treat it as hardening, not a fix. - Do not treat
--disallow-code-generation-from-stringsas a mitigation for this issue. The source states the escape bypasses it.
P2 (within 72 hours)
- Where vm2 is used only for first-party, trusted code, confirm that no path lets external input reach
vm.run. Trace every call site and the provenance of the string passed to it. - Add runtime egress controls around any process that hosts a
NodeVM: block outbound connections except to named destinations, and alert onchild_processspawns from the Node process. The PoC's final step isexecSync("sh"), so a shell child of a Node process is the observable to watch for. - Review CI/CD runners that execute repository-supplied JavaScript. A pull request that reaches a vm2 evaluator on a runner with cloud credentials is the highest-value target in most estates.
P3 (within 7 days)
- Track the upstream project for a fixed release. The source provides no patched version and states the access path was never patched despite repeated reports, so there is no version to pin to as of this advisory. The duplicate advisory's original text refers to "vm2 before 3.11.8", but the primary advisory does not confirm a fix at 3.11.8 and the duplicate was withdrawn. Do not assume 3.11.8 resolves this without verifying against the upstream repository.
- Where vm2 cannot be removed, evaluate an alternative isolation mechanism (a separate process, a container, or a different sandbox library) and plan the migration. Given the history of repeated escapes in this library, removal is the durable answer.
5. Indicators of compromise
No indicators of compromise available in the source material. The source provides a proof-of-concept code block and a prototype chain description, not atomic indicators from an observed intrusion. The PoC strings are reproduced in §6 for detection purposes only.
Behavioural indicators
| behaviour | where to observe | confidence |
|---|---|---|
Sandboxed JavaScript reads __proto__ via __lookupGetter__ with a numeric key (67 in the PoC) |
Node.js process instrumentation, runtime application self-protection, or a patched __lookupGetter__ wrapper |
Medium. Derived from the published PoC; no observed deployment data. |
Assignment to EventEmitter.prototype.emit from code that should be sandboxed |
Runtime instrumentation of the Node process, or a frozen EventEmitter.prototype that raises on write |
Medium. The PoC's core step. |
A sh or shell child process spawned by a Node process that hosts a sandbox evaluator |
EDR process-tree telemetry, execve auditing on Linux hosts |
Medium. The PoC's final action; legitimate applications may also spawn shells, so baseline first. |
process events (exit, unhandledRejection) triggered immediately after a sandboxed evaluation |
Application logs correlated with process lifecycle events | Low. The PoC uses these events but they occur normally in production. |
6. Detection
The source contains distinctive artefacts: the __lookupGetter__ call pattern with key 67, the getBuiltinModule("child_process") call, and the execSync("sh") invocation. These appear in the PoC and would appear in a weaponised variant that reuses the technique. A YARA rule on the JavaScript source or on files containing the sandboxed payload:
rule vm2_NodeVM_proto_escape_poc
{
meta:
author = "Adverse Trace"
date = "2026-10-06"
reference = "https://github.com/advisories/GHSA-88hf-g992-jg85"
description = "Detects JavaScript matching the published vm2 NodeVM sandbox escape proof of concept, which reaches the host __proto__ getter via console._stdout/_stderr and overwrites EventEmitter.prototype.emit to run a shell in the host process context."
strings:
$lookup = "__lookupGetter__" ascii
$key67 = "67,'__proto__'" ascii
$key67b = "67, '__proto__'" ascii
$stdout = "console._stdout" ascii
$stderr = "console._stderr" ascii
$emit = "EventEmitter.prototype.emit" ascii
$builtin = "getBuiltinModule(\"child_process\")" ascii
$exec = "execSync(\"sh\"" ascii
condition:
uint32(0) != 0x04034b50 and
filesize < 1MB and
$lookup and 2 of ($key67, $key67b, $stdout, $stderr, $emit, $builtin, $exec)
}
A Sigma rule for the process-level observable, a shell spawned by a Node process that hosts a sandbox evaluator:
title: Shell Spawned by Node.js Process Hosting Sandbox Evaluator
id: 7f3c1a92-4b6e-4d21-9c8a-2e5f0b7d1a44
status: experimental
description: Detects a shell child process spawned by a Node.js process, consistent with the final step of the vm2 NodeVM sandbox escape proof of concept. Baseline against legitimate shell usage before enabling as an alert.
author: Adverse Trace
date: 2026/10/06
references:
- https://github.com/advisories/GHSA-88hf-g992-jg85
logsource:
category: process_creation
product: linux
detection:
parent:
ParentImage|endswith:
- '/node'
- '/nodejs'
child:
Image|endswith:
- '/sh'
- '/bash'
- '/dash'
condition: parent and child
falsepositives:
- Build scripts and package managers that shell out from Node
- Applications that intentionally spawn shells for legitimate tasks
level: low
The Sigma rule is deliberately low severity because Node processes spawn shells legitimately in many build and deployment contexts. Tune the parent image path and add command-line conditions for your environment before raising it.
7. Sources
- GitHub Advisory Database, "vm2: Sandbox Escape (NodeVM)", GHSA-88hf-g992-jg85, published 2026-10-05, https://github.com/advisories/GHSA-88hf-g992-jg85
- GitHub Advisory Database, "Duplicate Advisory: Sandbox Escape (NodeVM)", GHSA-gg6f-mhqm-f7gp, withdrawn, https://github.com/advisories/GHSA-gg6f-mhqm-f7gp
- Prior vm2 advisories referenced by the primary source: GHSA-vwrp-x96c-mhwq, GHSA-v6mx-mf47-r5wg, GHSA-grj5-jjm8-h35p, GHSA-47x8-96vw-5wg6
8. Adverse Trace position
The GitHub Advisory Database scores this 10.0 Critical under CVSS v4, and the score is defensible for an internet-facing service that evaluates untrusted JavaScript. It overstates the risk for a library embedded in a desktop tool or a build step that only runs first-party code. No CISA KEV entry, no EPSS score, and no confirmed exploitation in the wild are available in the source material, and the source is a single GitHub advisory with a proof of concept, so the technical detail here is single-sourced; verify the prototype chain behaviour against your own vm2 version before acting on the containment steps. The upstream project has no fixed release in the source, and the advisory states the underlying __proto__ access was reported repeatedly without being patched, which is the fact that should drive the decision: treat vm2 as an isolation boundary that does not hold. Adverse Trace assesses that clients running vm2 in any path that touches untrusted input should move that evaluation to a process or container boundary rather than wait for a patch. We will monitor the upstream repository and the GitHub Advisory Database for a fixed version and reissue if one appears, and we will report any confirmed exploitation observed in client telemetry.
Published via PulseTrace — Adverse Trace threat intelligence.