# vm2 NodeVM can replace the host process TLS trust store

vm2 NodeVM with tls+url builtins lets sandbox code call tls.setDefaultCACertificates() via a host-realm array, replacing host trust roots and enabling MITM of host HTTPS clients.

- Payout: Research
- Severity: critical
- Program: vm2
- Found by: Forrof
- Disclosed: 2026-10-01
- CVE: CVE-2026-92941
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-98xx-8mx4-x7cm
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-98xx-8mx4-x7cm
- Tags: area:oss domain:oss impact:data-leak impact:integrity kind:research platform:ghsa sev:critical tech:nodejs technique:chained technique:source-review vuln:misconfiguration vuln:sandbox-escape

**Target:** NodeVM sandbox builtin bridge (lib/builtin.js) over Node.js tls module
**Root cause:** Non-dangerous builtins are exposed via a recursive read-only proxy that blocks property writes but not calls to functions mutating process-wide host state; tls.setDefaultCACertificates() was not classified dangerous like dns.setServers().
**Steps:**
1. Configure NodeVM allowing only `tls` and `url` builtins on Node.js >=22.19.0 or >=24.5.0.
2. In sandbox: `const tls = require('tls'); const {URLSearchParams} = require('url');`
3. Build host array: `new URLSearchParams('ca=' + encodeURIComponent(attackerCaPem)).getAll('ca')`
4. Call `tls.setDefaultCACertificates(hostArray)` — bridge unwraps mapped array to original host array, passing native type check.
5. Host-realm HTTPS client now trusts attacker CA; second request to loopback server signed by attacker CA returns 200 with marker `VM2_POC_HOST_TLS_RESPONSE_3e71c8`.
**How found:** Noted the dangerous-builtin list rejects dns for process-wide state mutation (dns.setServers) but not tls, which now exposes equivalent trust mutation; then found the read-only proxy only blocks property assignment, not function calls, and that URLSearchParams.getAll() yields a host-realm array that the bridge unwraps to satisfy the native type check.
**Exploitation:** Attacker submits sandbox code; using only allowed tls and url builtins, obtains a host-realm array via URLSearchParams.getAll() and passes it to tls.setDefaultCACertificates(), swapping the process-wide default CA list for an attacker CA. Subsequent host HTTPS clients then accept attacker-signed certs, allowing credential/token/session theft and response tampering when the attacker can influence DNS, routing, proxy, or destination.
**Escalation:** Trust-store replacement extends beyond the sandbox to all host TLS clients lacking explicit ca option; also removes legitimate roots causing DoS on unrelated host TLS traffic.
**Insight:** When auditing sandbox builtin allowlists, enumerate every exported function of allowed modules for process-wide side effects (setters on global state), not just property writes; and look for host-realm object factories (URLSearchParams, typed arrays) that produce values the bridge will unwrap back to host objects, bypassing native type checks.

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

**Sandbox code replacing host default CA list**

```
const tls = require('tls');
const {URLSearchParams} = require('url');

const hostArray = new URLSearchParams(
  'ca=' + encodeURIComponent(attackerCaPem)
).getAll('ca');

tls.setDefaultCACertificates(hostArray);
```

