The National Vulnerability Database has published CVE-2026-93606, a CVSS 10.0 (CRITICAL) vulnerability in vm2, the widely deployed npm sandbox library, affecting all versions 3.12.0 and earlier. This is a sandbox escape — the worst-case outcome for a sandboxing library — and it is remotely exploitable over the network wherever an application uses vm2's VM or NodeVM classes to execute untrusted code.
If your organization runs multi-tenant SaaS platforms, plugin/extension systems, user-supplied script execution, low-code workflow engines, or code evaluation APIs built on Node.js, there is a realistic chance vm2 is somewhere in your dependency tree — often as a transitive dependency you did not consciously choose. A sandbox escape here does not just break a security boundary: it hands an attacker arbitrary code execution in the host Node.js process, with all the privileges, credentials, and network reach that process has.
We are treating this with the same urgency we apply to internet-facing remote code execution, because functionally that is what it becomes. This post breaks down the vulnerability mechanics from a defender's perspective, gives you concrete detection content, and lays out a remediation path — including the harder conversation about whether vm2 should remain in your stack at all.
Technical Analysis
Affected Component and Versions
- Product:
vm2(npm package) - Affected versions: 3.12.0 and earlier
- Vulnerability type: Sandbox escape / improper neutralization across security realms (CWE-class: sandbox isolation failure)
- CVSS v3.1: 10.0 (CRITICAL) — network-exploitable, no privileges or user interaction required
- CVE: CVE-2026-93606 — NVD entry
How the Vulnerability Works
vm2's security model relies on a "bridge" (lib/bridge.js) that sanitizes objects crossing between the host realm and the sandbox realm. CVE-2026-93606 exposes a gap in that bridge's Promise handling.
The attack chain, from a defensive perspective:
- Precondition: The host application exposes a host API into the sandbox that returns a host-realm Promise. This is extremely common — think
fetch-style helpers, database clients, file APIs, or any async function the host deliberately makes available to sandboxed code. - The flaw: The bridge's rejection sanitizer — the
hostPromiseSanitizeReject/makeSanitizedPromiseCallback/normalizeHostPromiseCallbackscode path — only wrapsthen/catchrejection slots when those slots hold a function. Separately, the sandbox-side neutralization ofSymbol.speciesand.thenis installed only on the sandbox's intrinsicPromise.prototype. A host-realm Promise never passes through that neutralization. - The escape: Code running inside the sandbox can overwrite
p.constructor[Symbol.species]on the host Promise (the summary is truncated, but the implication is clear: by controlling the species constructor, sandboxed code causes the host realm to invoke an attacker-controlled constructor/callback in the host realm's execution context — outside the sandbox's sanitized wrappers). - Impact: Once attacker-controlled code executes in the host realm, the sandbox boundary is gone. The attacker has full Node.js execution in the host process: filesystem access,
child_process, environment variables (cloud credentials, database strings), and outbound network access.
The exploitation requirement — a host API returning a host Promise — is not exotic. It describes the canonical way developers are told to use vm2: expose a curated set of host functions to untrusted scripts. That is what makes this CVSS 10 rather than a theoretical edge case.
Why vm2 Deserves Special Scrutiny
Defenders with long memories will note that vm2 has suffered a recurring pattern of sandbox escapes, and the project's maintenance status has been a known concern in the Node.js security community. Even with a patch for CVE-2026-93606, the architectural reality stands: vm2 attempts to build a security boundary inside a single V8 isolate using proxy tricks and intrinsic patching. CVE-2026-93606 is a textbook demonstration of why that approach fails — the patch surface is effectively infinite, because every host-object interaction is a potential realm-confusion bug. The durable fix is process- or isolate-level isolation (e.g., isolated-vm, worker processes with IPC, containers, or microVMs), not another bridge patch.
Exploitation Status
At time of writing, the NVD entry is freshly published and we are not aware of confirmed in-the-wild exploitation or CISA KEV inclusion. However: the vulnerability class (sandbox escape), the attack surface (untrusted code execution is vm2's entire purpose), and the detailed public description of the flawed code path mean proof-of-concept development should be assumed imminent. Treat exploitation as a near-term certainty for any internet-reachable service using vm2, and prioritize accordingly.
Detection & Response
Detecting the escape primitive itself inside the V8 realm is not realistic from telemetry. What you can detect — and should hunt for retroactively — is post-escape behavior: a Node.js process doing things a sandboxed workload should never do. The most reliable signal is Node.js spawning child processes or making unexpected network connections.
Sigma Rules
The following rules target the highest-fidelity post-exploitation behaviors. The Windows rule applies if you run Node services on Windows hosts; the Linux process rules map to Sysmon-for-Linux or auditd process creation telemetry.
---
title: Node.js Process Spawning Shell or System Utility
id: 3f8c1a72-4b6e-4d21-9a07-5c2e8f1b9034
status: experimental
description: Detects Node.js runtime spawning a shell or common post-exploitation utility, consistent with a vm2 sandbox escape (CVE-2026-93606) leading to child_process execution in the host process.
references:
- https://nvd.nist.gov/vuln/detail/CVE-2026-93606
- https://attack.mitre.org/techniques/T1059/
author: Security Arsenal
date: 2026/04/06
tags:
- attack.execution
- attack.t1059.007
logsource:
category: process_creation
product: windows
detection:
selection_parent:
ParentImage|endswith:
- '\node.exe'
- '\nodejs.exe'
selection_child:
Image|endswith:
- '\cmd.exe'
- '\powershell.exe'
- '\pwsh.exe'
- '\wscript.exe'
- '\cscript.exe'
- '\rundll32.exe'
- '\certutil.exe'
- '\bitsadmin.exe'
- '\curl.exe'
- '\whoami.exe'
- '\net.exe'
condition: selection_parent and selection_child
falsepositives:
- Node.js build tooling (npm scripts, node-gyp) on developer workstations
- Legitimate application features that shell out (image processing, PDF generation)
level: high
---
title: Linux Node.js Process Spawning Shell or Recon Utility
id: 8b2d4e19-7c3a-4f58-b612-9d4a6e0c2751
status: experimental
description: Detects a Node.js process on Linux spawning shells or reconnaissance/utility binaries, consistent with post-exploitation after a vm2 sandbox escape (CVE-2026-93606).
references:
- https://nvd.nist.gov/vuln/detail/CVE-2026-93606
- https://attack.mitre.org/techniques/T1059/
author: Security Arsenal
date: 2026/04/06
tags:
- attack.execution
- attack.t1059.004
logsource:
category: process_creation
product: linux
detection:
selection_parent:
ParentImage|endswith:
- '/node'
selection_child:
Image|endswith:
- '/sh'
- '/bash'
- '/dash'
- '/zsh'
- '/curl'
- '/wget'
- '/nc'
- '/ncat'
- '/python'
- '/python3'
- '/perl'
- '/whoami'
- '/id'
- '/uname'
- '/base64'
condition: selection_parent and selection_child
falsepositives:
- Node applications legitimately invoking system commands (ffmpeg wrappers, git tooling)
- Container health checks implemented via shell
level: high
---
title: Node.js Loading Child Process Execution After Script Evaluation
id: c17a9e05-2d84-4f3b-a561-7e0b3d9f8246
status: experimental
description: Detects Node.js executing inline-evaluated script content combined with child process activity, a pattern consistent with untrusted-code evaluation services (a common vm2 use case) being escaped via CVE-2026-93606.
references:
- https://nvd.nist.gov/vuln/detail/CVE-2026-93606
- https://attack.mitre.org/techniques/T1059.007/
author: Security Arsenal
date: 2026/04/06
tags:
- attack.execution
- attack.t1059
logsource:
category: process_creation
product: windows
detection:
selection:
Image|endswith:
- '\node.exe'
CommandLine|contains:
- '-e "'
- '--eval'
- 'child_process'
filter_legit:
CommandLine|contains:
- 'npm'
- 'node_modules\\.bin'
condition: selection and not filter_legit
falsepositives:
- Developers running ad-hoc node -e snippets
- CI/CD pipeline steps
level: medium
KQL — Microsoft Sentinel / Defender
This query hunts across endpoint telemetry for Node.js processes exhibiting post-escape behavior: spawning shells/utilities or establishing unusual outbound connections. It assumes Defender for Endpoint (DeviceProcessEvents / DeviceNetworkEvents); for Linux servers ingested via Syslog/CEF, adapt against the Syslog table with ProcessName == 'node'.
let NodeChildren = DeviceProcessEvents
| where InitiatingProcessFileName in~ ("node.exe", "node", "nodejs")
| where FileName in~ ("cmd.exe", "powershell.exe", "pwsh.exe", "sh", "bash", "dash",
"curl.exe", "curl", "wget", "nc", "ncat", "certutil.exe",
"whoami.exe", "whoami", "id", "uname", "python", "python3", "base64")
| project ChildTime=TimeGenerated, DeviceName, DeviceId,
ChildProcess=FileName, ChildCommand=ProcessCommandLine,
NodeCommand=InitiatingProcessCommandLine,
Account=InitiatingProcessAccountName, NodePid=InitiatingProcessId;
let NodeNet = DeviceNetworkEvents
| where InitiatingProcessFileName in~ ("node.exe", "node", "nodejs")
| where RemotePort in (80, 443, 4444, 8080, 8443) and RemoteIPType == "Public"
| project NetTime=TimeGenerated, DeviceId, RemoteUrl, RemoteIP, RemotePort,
NodePid=InitiatingProcessId;
NodeChildren
| join kind=leftouter NodeNet on DeviceId, NodePid
| extend FirstSeen = min_of(ChildTime, NetTime)
| order by FirstSeen desc
Tune the remote-port and child-process lists against your baseline. In mature environments, alert on any public egress from a Node process that has not previously been observed making outbound connections — that is often the cleanest signal that a sandboxed workload is now reaching out to attacker infrastructure.
Velociraptor VQL
Use this hunt artifact to sweep your Node.js fleet for live processes with suspicious parent/child relationships and exposed dependency state. Pair it with a package-audit collection to identify vulnerable vm2 versions in deployed node_modules trees.
-- Hunt for Node.js processes with suspicious children or command lines
-- relevant to vm2 sandbox escape post-exploitation (CVE-2026-93606)
SELECT Pid, Ppid, Name, CommandLine, Exe, Username, CreateTime
FROM pslist()
WHERE Name =~ '(?i)node'
AND (
CommandLine =~ '(?i)(--eval|child_process|vm2)'
OR Ppid IN (
SELECT Pid FROM pslist()
WHERE Name =~ '(?i)(sh|bash|dash|cmd|powershell|pwsh)'
)
)
-- Sweep deployed node_modules for vulnerable vm2 versions (<= 3.12.0)
SELECT FullPath, Btime, Mtime,
read_file(filename=FullPath) AS PackageJSON
FROM glob(globs='**/node_modules/vm2/package.json',
root='C:/')
WHERE PackageJSON =~ '"version"\\s*:\\s*"(0\\.|1\\.|2\\.|3\\.(0|1[0-2])\\.)'
Adjust the glob root per platform (/ on Linux). The version regex catches 3.12.x and earlier; validate parse edge cases in your environment before fleet-wide execution.
Remediation / Verification Script
The following Bash script inventories Node.js projects for vulnerable vm2 versions, including transitive dependencies, and reports findings. Run it across build servers, container images, and deployed hosts.
#!/usr/bin/env bash
# CVE-2026-93606 vm2 exposure scanner — Security Arsenal
# Scans for vm2 in package manifests and lockfiles; flags versions <= 3.12.0
set -euo pipefail
ROOT="${1:-/srv}"
echo "[+] Scanning ${ROOT} for vm2 dependencies..."
# 1. Find installed vm2 copies and extract versions
found=0
while IFS= read -r pkg; do
ver=$(grep -oE '"version"[[:space:]]*:[[:space:]]*"[0-9.]+"' "$pkg" | grep -oE '[0-9.]+' | head -1)
major=$(cut -d. -f1 <<<"$ver"); minor=$(cut -d. -f2 <<<"$ver")
if [ "$major" -lt 3 ] || { [ "$major" -eq 3 ] && [ "$minor" -le 12 ]; }; then
echo "[!] VULNERABLE vm2 ${ver} at: ${pkg}"
found=1
else
echo "[.] vm2 ${ver} (check vendor advisory for fixed version): ${pkg}"
fi
done < <(find "$ROOT" -type f -path '*/node_modules/vm2/package.json' 2>/dev/null)
# 2. Check lockfiles for transitive vm2 references
while IFS= read -r lock; do
if grep -q '"vm2"' "$lock"; then
echo "[!] vm2 referenced in lockfile (may be transitive): ${lock}"
grep -oE '"vm2"[^}]*"version"[[:space:]]*:[[:space:]]*"[0-9.]+"' "$lock" | head -5 || true
found=1
fi
done < <(find "$ROOT" -maxdepth 6 -name 'package-lock.json' -o -maxdepth 6 -name 'yarn.lock' -o -maxdepth 6 -name 'pnpm-lock.yaml' 2>/dev/null)
# 3. Running Node processes that may be live-exploitable
echo "[+] Active node processes:"
ps -eo pid,user,args | grep -E '[n]ode' || echo " (none)"
[ "$found" -eq 0 ] && echo "[+] No vulnerable vm2 instances found under ${ROOT}." \
|| { echo "[!] ACTION REQUIRED: patch or replace vm2 — see remediation steps."; exit 1; }
For containerized workloads, add npm audit --package=vm2 (or npm ls vm2 to surface the dependency chain) to your CI pipeline as a blocking gate, and scan images with your registry scanner — vm2 ships inside more third-party images than most teams realize.
Remediation
- Identify exposure immediately. Enumerate every application — first-party and vendor — that executes user-supplied or tenant-supplied JavaScript. Run
npm ls vm2across repos and deployed artifacts to catch transitive inclusion. Don't trust that you "don't use vm2"; verify. - Patch to the fixed release. Upgrade vm2 to the version designated as fixed in the vendor/GitHub security advisory referenced from the NVD entry for CVE-2026-93606. Confirm the lockfile, container image, and any vendored
node_modulesare all rebuilt — apackage.jsonbump without a redeployed artifact changes nothing. - Plan the migration away from vm2. A patch closes this hole; it does not fix the architecture. vm2's realm-confusion attack surface has produced repeated escapes, and CVE-2026-93606's root cause — host-realm objects crossing the bridge — is structural. Migrate untrusted-code execution to
isolated-vm(separate V8 isolates), dedicated worker processes, or containerized execution with seccomp/AppArmor, dropped capabilities, no inherited environment secrets, and egress filtering. - Reduce blast radius now, if patching is delayed:
- Do not expose any host API that returns host-realm Promises into the sandbox — this is the exploitation precondition. If you can gate the vulnerable path (disable async host functions exposed to sandboxed code), do it.
- Run the Node.js process hosting vm2 as a non-root, unprivileged user with a minimal filesystem view.
- Strip secrets (cloud metadata access, env-var credentials) from the process environment; prefer short-lived, injected credentials.
- Apply egress filtering so a compromised Node process cannot reach arbitrary internet destinations or internal services.
- Network-segment untrusted-code-execution services away from databases and internal APIs.
- Hunt retroactively. Deploy the Sigma, KQL, and VQL content above and look back at least 30 days for Node processes spawning shells or initiating novel outbound connections. A sandbox escape that already happened will not announce itself.
- Watch for KEV listing. Given CVSS 10 and the network-exploitable pathway, monitor CISA's Known Exploited Vulnerabilities catalog; a listing would impose remediation deadlines for federal environments and should trigger emergency-change procedures everywhere else.
If you need help scoping exposure across a large Node.js estate, validating whether a compromise already occurred, or redesigning your untrusted-code execution architecture, our DFIR and offensive teams do this work every week.
Related Resources
Security Arsenal Penetration Testing Services AlertMonitor Platform Book a SOC Assessment vulnerability-management Intel Hub
Is your security operations ready?
Get a free SOC assessment or see how AlertMonitor cuts through alert noise with automated triage.