# vm2 crypto builtin loads attacker native code through setEngine

vm2's read-only crypto bridge exposes setEngine, letting sandboxed plugins load a bundled native library whose constructor runs host code before OpenSSL validation fails.

- Payout: Research
- Severity: critical
- Program: vm2
- Found by: Forrof
- Disclosed: 2026-10-01
- CVE: CVE-2026-92939
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-46pr-c5wc-xffx
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-46pr-c5wc-xffx
- Tags: area:oss domain:oss impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:nodejs technique:source-review vuln:rce vuln:sandbox-escape

**Target:** NodeVM sandbox builtin bridge (crypto module)
**Root cause:** Generic builtin loader wraps non-dangerous builtins in a read-only proxy that blocks property assignment but not callable authority; crypto.setEngine forwards a filesystem path to OpenSSL's dynamic engine loader, which executes library constructors before validating engine symbols.
**Steps:**
1. Configure NodeVM allowing only the `crypto` builtin.
2. Place an attacker-supplied plugin package containing a native library (e.g. `probe-engine.so`) on disk.
3. From sandboxed JS call `crypto.setEngine(pathToBundledLibrary)`.
4. vm2 forwards to host crypto; OpenSSL asks the OS loader to dlopen the file, running its constructor.
5. Observe `ERR_CRYPTO_ENGINE_UNKNOWN` yet marker file `vm2-setengine-native-marker.txt` containing `VM2_SETENGINE_NATIVE_CODE_EXECUTED` exists.
**How found:** Noticed the generic builtin loader exposes unwrapped builtins via a read-only proxy that only blocks property assignment, not call authority; then identified crypto.setEngine as a callable that hands a path to OpenSSL's dynamic loader, where constructors run before engine validation.
**Exploitation:** Attacker ships a native library inside an untrusted plugin package; when the host runs the plugin's JS in NodeVM with crypto allowed, the plugin calls crypto.setEngine on the bundled library path. The OS loader executes the constructor with host-process privileges before OpenSSL rejects the file, yielding arbitrary native code execution outside all vm2 restrictions.
**Escalation:** Replace marker-only constructor with code reading secrets/env/credentials, modifying files for persistence, reaching internal services via host network identity, or stealing other tenants' data in the same process.
**Insight:** Read-only proxies over host modules only stop writes, not dangerous callable exports — audit every allowed builtin's functions for ones that take filesystem paths or trigger native loading (setEngine, dlopen-style APIs), since a package file already on disk becomes an execution primitive even without fs.

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

**Native marker library constructor PoC**

```
cc -dynamiclib -O2 -o probe-engine.dylib engine_probe.c
   node poc.js ./probe-engine.dylib

   On Linux:

   cc -shared -fPIC -O2 -o probe-engine.so engine_probe.c
   node poc.js ./probe-engine.so
```

**Sandboxed crypto.setEngine call flow**

```
plugin calls crypto.setEngine(pathToBundledLibrary)
  -> vm2 forwards the call to the host crypto module
  -> OpenSSL asks the operating-system loader to load the library
  -> the library constructor executes native code in the host process
```

