vm2: NodeVM builtin:['*'] Exposes os and dns — Host Process Exposure From the Sandbox

Vulnerability / Supply Chain

A critical vulnerability in the vm2 npm package breaks the sandbox isolation that NodeVM is designed to enforce. When configured with builtin: ['*'], two Node.js built-in modules — os and dns — slip through the dangerous-builtin filter. Both expose host-process state that the vm2 boundary cannot contain, and both carry write APIs that mutate global host-process state from the sandbox.

Description

A critical vulnerability affects the vm2 npm package when NodeVM is configured with builtin: ['*'] — the documented "full builtins" pattern recommended in the README and used in the test fixtures. Under this configuration, two Node.js built-in modules — os and dns — are admitted into the sandbox even though they expose host-process state that the vm2 boundary cannot contain.

Both modules are reachable from inside the sandbox. Both expose state belonging to the entire host process rather than sandbox-local state. And both carry write APIs that mutate global host-process state from the sandbox: os.setPriority(), dns.setServers(), and dns.setDefaultResultOrder().

The issue is tracked as GHSA-m5w8-4gq2-6f8x and rated Critical. It is classified as a sibling of GHSA-9g8x-92q2-p28f, the prior advisory that closed the "process-wide observability builtins" class but left these two modules out of the fix.

An attacker must first be able to execute attacker-controlled JavaScript inside the NodeVM sandbox. In the vulnerable configuration, the attack flow is:

Attacker-controlled JavaScript → executed inside vulnerable NodeVM → builtin: ['*'] exposes os/dns → host information disclosure or DNS manipulation.

What Happened

The prior advisory, GHSA-9g8x-92q2-p28f, closed a class of "process-wide observability builtins" by adding diagnostics_channel, async_hooks, perf_hooks, and v8 to the DANGEROUS_BUILTINS set in lib/builtin.js. The fix's rationale, documented in the commit message and docs/ATTACKS.md Category 35, was general:

Process-wide observability builtins. Unlike most Node builtins, these expose state of the entire host process rather than sandbox-local state — the vm2 boundary cannot usefully contain them because the data they surface […] belongs to the embedder. Even a readonly proxy that forwards every call to the host module is a working host-data exfiltration primitive.

Two builtins satisfying the exact same description were not added: os and dns. Both are reachable today under the documented builtin: ['*'] configuration. Both expose host-process state that the vm.readonly() proxy cannot localise. And both have write APIs that mutate global host-process state from the sandbox.

The dns.setServers() call in particular turns sandbox code into a process-wide DNS hijack primitive — the advisory describes it as strictly worse than every read-only leak that GHSA-9g8x added.

The fix adds os and dns to DANGEROUS_BUILTINS, extending the same protection to the rest of the class. The existing isDangerousBuiltin(key) family-prefix matcher, introduced by GHSA-rp36-8xq3-r6c4, automatically catches node:os, node:dns, and node:dns/promises once the family names are present.

Why This Matters

The purpose of vm2 is to run JavaScript inside a sandbox and isolate it from the host environment. In the affected configuration, os and dns can access or modify information belonging to the host process. This means the sandbox does not provide the expected isolation for these particular capabilities.

The most significant capability described by the advisory is dns.setServers(), which can modify the DNS resolver configuration used by the host process. Subsequent DNS lookups made by that process can be directed through the configured resolver.

Having a vulnerable vm2 version alone does not mean the application is directly exploitable. The application must actually use the affected NodeVM configuration (builtin: ['*']) and provide a way for attacker-controlled JavaScript to reach the sandbox.

Affected Packages

PackageEcosystemAffected versionsFixed version
vm2npm<= 3.11.5>= 3.11.6

The vulnerability affects all NodeVM configurations that expand the builtin allowlist via '*' and have not manually appended -os or -dns exclusions — which is the recommended configuration in the README and the test fixtures.

  • Advisory ID: GHSA-m5w8-4gq2-6f8x
  • Severity: Critical
  • CWE: CWE-200 (Exposure of Sensitive Information to an Unauthorized Actor), CWE-732 (Incorrect Permission Assignment for Critical Resource), CWE-285 (Improper Authorization)

Potentially Affected Scenarios

The advisory is relevant when both of the following conditions exist:

  1. The application uses NodeVM with builtin: ['*'].
  2. Attacker-controlled JavaScript can be executed inside that NodeVM sandbox.

Environments where untrusted or less-trusted JavaScript is executed inside NodeVM with the full builtins configuration are at risk. A vulnerable version of vm2 alone does not establish that the application is directly exploitable.

Attack Details

The attack requires attacker-controlled JavaScript to execute inside the affected NodeVM. The simplified attack flow is:

Attacker-controlled JavaScript → NodeVM sandbox → builtin: ['*'] → access to os/dns → host information disclosure or DNS manipulation.

How the modules reach the sandbox

Under builtin: ['*'], the BUILTIN_MODULES list in lib/builtin.js is built by filtering Node.js's own builtinModules against the DANGEROUS_BUILTINS set:

const BUILTIN_MODULES = (nmod.builtinModules || Object.getOwnPropertyNames(process.binding('natives')))
  .filter(s => !s.startsWith('internal/') && !s.startsWith('_') && !isDangerousBuiltin(s));

The DANGEROUS_BUILTINS set at the time of the vulnerability:

const DANGEROUS_BUILTINS = new Set([
  'module', 'worker_threads', 'cluster', 'vm', 'repl',
  'inspector', 'process', 'trace_events', 'wasi',
  // GHSA-9g8x-92q2-p28f:
  'diagnostics_channel', 'async_hooks', 'perf_hooks', 'v8'
]);

os and dns are absent. Under builtin: ['*'] they are admitted into the user-visible builtin map and loaded via the default vm.readonly(hostRequire(key)) path:

builtins.set(key, special ? special : vm => vm.readonly(hostRequire(key)));

The readonly proxy forwards every method call to the host realm. For modules whose entire purpose is to read or mutate host-process state, the readonly wrap protects nothing — the same observation the GHSA-9g8x commit message makes for v8 and perf_hooks.

Advisory-identified capabilities

The advisory identifies the following capabilities exposed through these modules:

  • os.userInfo() — exposes host username, UID, GID, home directory and shell
  • os.networkInterfaces() — exposes host network interfaces, including IP and MAC information
  • os.hostname() — exposes the host hostname
  • os.loadavg(), os.uptime(), os.freemem() and os.totalmem() — expose host system information
  • os.setPriority() — can modify the host process priority
  • dns.getServers() — exposes configured DNS servers
  • dns.setServers() — modifies the DNS resolver list used by the host process
  • dns.setDefaultResultOrder() — modifies DNS result ordering

Host Capabilities Exposed

os module

The os module can expose information about the actual host process, including:

  • Username
  • UID/GID
  • Home directory
  • Shell
  • Hostname
  • Network interfaces
  • IP addresses
  • MAC addresses
  • System memory and uptime information

It also provides os.setPriority(), which can modify the host process priority.

dns module

The dns module can expose or modify DNS configuration belonging to the host process. Most importantly, dns.setServers() can replace the host process's DNS resolver list, meaning subsequent DNS lookups made by that process can be sent through the configured resolver.

The advisory describes dns.setServers() as a process-wide write primitive and identifies it as strictly worse than every read-only leak that the prior GHSA-9g8x fix addressed.

The dns/promises subpath also exists and shares the same module surface. Adding dns to DANGEROUS_BUILTINS automatically catches dns/promises via the existing family-prefix matcher.

What Data Is at Risk

Based directly on the advisory, the exposed information includes:

  • Host identity: username, UID, GID, home directory and shell.
  • Network information: host network interfaces, IP addresses and MAC addresses.
  • Host information: hostname, uptime, memory and load information.
  • DNS configuration: configured DNS servers.

The advisory also identifies DNS manipulation as an integrity risk because dns.setServers() can change the resolver configuration used by the host process.

Risks

The primary risks identified by the advisory are:

1. Host Information Disclosure

An attacker executing JavaScript inside the affected sandbox can access information belonging to the host process through the os module.

2. Network Topology Disclosure

os.networkInterfaces() can expose the host's network interfaces, including IP and MAC addresses.

3. Process-Wide DNS Manipulation

dns.setServers() can modify the DNS resolver list used by the host process. Subsequent DNS lookups can therefore be directed through the configured resolver. The advisory describes this as a process-wide DNS hijack primitive — strictly worse than every read-only leak that GHSA-9g8x added.

4. Host Process Priority Modification

os.setPriority() can modify the priority of the host process.

Recommended Actions

Upgrade vm2

Upgrade from the affected versions to:

vm2 3.11.6 or later.

The fix adds os and dns to DANGEROUS_BUILTINS, closing the class. The existing family-prefix matcher automatically covers node:os, node:dns, and node:dns/promises.

Verify Application Configuration

Check whether the application actually uses:

NodeVM({
  require: {
    builtin: ['*']
  }
})

Also verify whether attacker-controlled JavaScript can reach and execute inside the NodeVM. A vulnerable version of vm2 alone does not establish that the application is directly exploitable.

Register Safe Wrappers If Needed

Embedders who genuinely need a sandbox-local os or dns (typically os.platform(), os.EOL, os.constants) can register a hand-written safe wrapper under those names via the mock / override escape hatch, mirroring the approach documented for the GHSA-9g8x denials.

If Exploitation Is Suspected

The advisory recommends reviewing the environment and rotating credentials if exploitation is suspected, particularly because DNS manipulation can affect subsequent host-process DNS resolution.

The Fix

The fix adds os and dns to DANGEROUS_BUILTINS:

const DANGEROUS_BUILTINS = new Set([
  'module', 'worker_threads', 'cluster', 'vm', 'repl',
  'inspector', 'process', 'trace_events', 'wasi',
  'diagnostics_channel', 'async_hooks', 'perf_hooks', 'v8',
  // SECURITY (this advisory):
  // `os.userInfo()` / `os.networkInterfaces()` leak host process identity
  // and network topology in the same class as the GHSA-9g8x readers.
  // `os.setPriority()`, `dns.setServers()`, and `dns.setDefaultResultOrder()`
  // are write primitives that mutate host-process state from the sandbox.
  // `dns.setServers()` is a process-wide DNS resolver hijack reachable in
  // one line of sandbox code.
  'os', 'dns'
]);
A vulnerable version of vm2 alone does not establish that the application is directly exploitable. The application must actually use the affected NodeVM configuration and provide a way for attacker-controlled JavaScript to reach it.
Frequently Asked Questions
QIs my application exploitable just because it uses vm2?
Not necessarily. The advisory is relevant only when both of the following conditions exist: the application uses NodeVM with builtin: ['*'], and attacker-controlled JavaScript can be executed inside that NodeVM sandbox. A vulnerable version of vm2 alone does not establish that the application is directly exploitable.
QWhy are os and dns dangerous in the sandbox?
The purpose of vm2 is to run JavaScript inside a sandbox and isolate it from the host environment. In the affected configuration, os and dns can access or modify information belonging to the host process. This means the sandbox does not provide the expected isolation for these particular capabilities. os exposes host identity, network interfaces, and system information, while dns.setServers() can modify the DNS resolver configuration used by the host process.
QWhat is dns.setServers() and why is it significant?
dns.setServers() is the most significant capability described by the advisory. It can replace the host process's DNS resolver list, meaning subsequent DNS lookups made by that process can be sent through the configured resolver. The advisory describes it as a process-wide DNS hijack primitive — strictly worse than every read-only leak that the prior GHSA-9g8x fix addressed.
QWhat is the relationship between this advisory and GHSA-9g8x-92q2-p28f?
GHSA-9g8x-92q2-p28f closed the "process-wide observability builtins" class by adding diagnostics_channel, async_hooks, perf_hooks, and v8 to DANGEROUS_BUILTINS. This advisory (GHSA-m5w8-4gq2-6f8x) is a sibling: it identifies that os and dns satisfy the exact same description but were not added in the prior fix. Adding them now extends the same protection to the rest of the class.
QI need os.platform() inside my sandbox. Will the fix break my application?
Possibly, if you were relying on builtin: ['*'] to expose the full os module. The fix adds os to DANGEROUS_BUILTINS, which blocks it entirely. The escape hatch is to register a hand-written safe wrapper under the os name via the mock / override configuration, exposing only the specific functions you need (such as os.platform(), os.EOL, or os.constants) without granting access to os.userInfo() or os.networkInterfaces().
QWhat should I do if I suspect exploitation occurred before upgrading?
The advisory recommends reviewing the environment and rotating credentials if exploitation is suspected, particularly because DNS manipulation can affect subsequent host-process DNS resolution.