CVE-2026-100721
vm2 before 3.12.2 Authorization Bypass via Custom Resolver
vm2 before 3.12.2 contains an authorization bypass in the NodeVM external-module resolver. When an embedder configures require.external with a custom resolver (and context: 'host'), LegacyResolver.customResolve in lib/resolver-compat.js records the resolved module directory in this.externals as new RegExp('^' + escapeRegExp(resolvedPath)), without requiring a path separator or end-of-string boundary. Untrusted guest code can therefore require the allowlisted module (e.g. foo) and then require the absolute path of a non-allowlisted sibling whose path merely shares the resolved prefix (e.g. .../node_modules/foo2/index.js); the sibling passes isPathAllowedForModule and is loaded through hostRequire, so its top-level code runs in the host process before the exports are wrapped with vm.readonly, resulting in a sandbox escape and arbitrary code execution in the host context.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
9.5
vm2 before 3.12.2 Authorization Bypass via Custom Resolver
vm2 before 3.12.2 contains an authorization bypass in the NodeVM external-module resolver. When an embedder configures require.external with a custom resolver (and context: 'host'), LegacyResolver.customResolve in lib/resolver-compat.js records the resolved module directory in this.externals as new RegExp('^' + escapeRegExp(resolvedPath)), without requiring a path separator or end-of-string boundary. Untrusted guest code can therefore require the allowlisted module (e.g. foo) and then require the absolute path of a non-allowlisted sibling whose path merely shares the resolved prefix (e.g. .../node_modules/foo2/index.js); the sibling passes isPathAllowedForModule and is loaded through hostRequire, so its top-level code runs in the host process before the exports are wrapped with vm.readonly, resulting in a sandbox escape and arbitrary code execution in the host context.
- Record
CVE-2026-100721- CVSS 4.0
- 9.5 Critical
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - CVSS 3.1
- 9.0 Critical
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H - Weakness
- CWE-863
- Updated
- 2026-11-02
EUVDEUVD-2026-87858
9.5
vm2 before 3.12.2 contains an authorization bypass in the NodeVM external-module resolver. When an embedder configures require.external with a custom resolver (and context: 'host'), LegacyResolver.customResolve in lib/resolver-compat.js records the resolved module directory in this.externals as new RegExp('^' + escapeRegExp(resolvedPath)), without requiring a path separator or end-of-string boundary. Untrusted guest code can therefore require the allowlisted module (e.g. foo) and then require the absolute path of a non-allowlisted sibling whose path merely shares the resolved prefix (e.g. .../node_modules/foo2/index.js); the sibling passes isPathAllowedForModule and is loaded through hostRequire, so its top-level code runs in the host process before the exports are wrapped with vm.readonly, resulting in a sandbox escape and arbitrary code execution in the host context.
- Record
EUVD-2026-87858- CVSS 4.0
- 9.5 Critical
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-5h3f-q97h-ccvc
9.5
vm2: NodeVM custom resolution bypasses external path boundaries
Summary
At source revision 91034466bfb7f56b95fd48083ec6ca36d058f164 of vm2 3.11.8, an untrusted NodeVM guest can turn one allowlisted custom-resolved module into authorization for a separate file whose path merely shares the resolved path's string prefix. LegacyResolver.customResolve stores the resolved path in this.externals as ^<path> without an end or separator boundary; a later absolute require of a sibling such as foo2/index.js therefore passes the external check and is loaded through hostRequire when the configured context is host. The decisive attack loaded foo as FOO_OK and then executed the prefix-sharing sibling, which returned PREFIX_PWN after invoking child_process; an otherwise identical control denied the sibling with ENOTFOUND.
Technical Details
The affected configuration is a documented NodeVM use in which the embedder sets require.external to {modules: ['foo'], transitive: false}, supplies a custom resolver that returns the foo directory, sets a root directory, and uses context: 'host'. The guest controls the require specifiers and requests the allowlisted bare name before requesting the absolute path of the prefix-sharing sibling. The test entry point is deliberately outside the configured root so ordinary node_modules lookup misses and the custom resolver is consulted.
The source-to-sink path is:
NodeVM.runexecutes guest code and creates the module-specificrequirefunction atlib/nodevm.js:516-575.DefaultResolver.resolveFullsearches normal locations and then dispatches a miss tocustomResolveatlib/resolver.js:227-316.LegacyResolver.customResolvechecks the bare specifier against the external cache, calls the embedder's resolver, and appends an authorization regular expression atlib/resolver-compat.js:266-291.isPathAllowedForModulefalls back tothis.externals.some(regex => regex.test(path))atlib/resolver-compat.js:193-215. The dynamically appended expression^/…/node_modules/fooalso matches/…/node_modules/foo2/index.jsbecause no path separator or end-of-string condition is required.loadJSusesthis.hostRequire(filename)for a host-context file and only wraps the returned exports withvm.readonlyatlib/resolver-compat.js:255-263. Top-level effects of the required file therefore occur in the host process before the exports are wrapped.
The relevant current code has the same flaw for both supported custom-resolver return shapes:
if (typeof resolved === 'string') {
this.externals.push(new RegExp('^' + escapeRegExp(resolved)));
return this.loadAsFileOrDirectory(resolved, extList);
}
const {module=x, path: resolvedPath} = resolved;
this.externals.push(new RegExp('^' + escapeRegExp(resolvedPath)));
return this.loadNodeModules(module, [resolvedPath], extList);
The existing anchored bare-specifier matcher and the separator-aware mod.path check address different authorization states. They do not constrain the new this.externals entries created after custom resolution. The violated invariant is that a resolved allowlisted path may authorize only that exact path and its descendants after a path separator, never a sibling selected by raw string prefix.
PoV
The minimal guest operation is to load the configured bare name and then request the separate absolute sibling path:
const foo = require('foo');
module.exports = {
foo,
sibling: require('/tmp/vm2-prefix-case/node_modules/foo2/index.js')
};
The corresponding embedder configuration is:
const vm = new NodeVM({
require: {
external: {modules: ['foo'], transitive: false},
root: '/tmp/vm2-prefix-case',
context: 'host',
resolve(name) {
return name === 'foo'
? '/tmp/vm2-prefix-case/node_modules/foo'
: undefined;
}
}
});
The sibling is not itself allowlisted. Its successful load is the authorization violation; the child_process call in its top-level code demonstrates that the file ran in the…
- Record
GHSA-5h3f-q97h-ccvc- CVSS 4.0
- 9.5 Critical
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - CVSS 3.1
- 10.0 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H - Weakness
- CWE-863
- Updated
- 2026-11-02
GitHub AdvisoriesGHSA-c9wr-qm7p-p6vc
9.5
Duplicate Advisory: vm2: NodeVM custom resolution bypasses external path boundaries
Duplicate Advisory
This advisory has been withdrawn because it is a duplicate of GHSA-5h3f-q97h-ccvc. This link is maintained to preserve external references.
Original Description
vm2 before 3.12.2 contains an authorization bypass in the NodeVM external-module resolver. When an embedder configures require.external with a custom resolver (and context: 'host'), LegacyResolver.customResolve in lib/resolver-compat.js records the resolved module directory in this.externals as new RegExp('^' + escapeRegExp(resolvedPath)), without requiring a path separator or end-of-string boundary. Untrusted guest code can therefore require the allowlisted module (e.g. foo) and then require the absolute path of a non-allowlisted sibling whose path merely shares the resolved prefix (e.g. .../node_modules/foo2/index.js); the sibling passes isPathAllowedForModule and is loaded through hostRequire, so its top-level code runs in the host process before the exports are wrapped with vm.readonly, resulting in a sandbox escape and arbitrary code execution in the host context.
- Record
GHSA-c9wr-qm7p-p6vc- CVSS 4.0
- 9.5 Critical
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 9.0 Critical
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H - Weakness
- CWE-863
- Updated
- 2026-11-02
NVD
9.5
vm2 before 3.12.2 contains an authorization bypass in the NodeVM external-module resolver. When an embedder configures require.external with a custom resolver (and context: 'host'), LegacyResolver.customResolve in lib/resolver-compat.js records the resolved module directory in this.externals as new RegExp('^' + escapeRegExp(resolvedPath)), without requiring a path separator or end-of-string boundary. Untrusted guest code can therefore require the allowlisted module (e.g. foo) and then require the absolute path of a non-allowlisted sibling whose path merely shares the resolved prefix (e.g. .../node_modules/foo2/index.js); the sibling passes isPathAllowedForModule and is loaded through hostRequire, so its top-level code runs in the host process before the exports are wrapped with vm.readonly, resulting in a sandbox escape and arbitrary code execution in the host context.
- Record
CVE-2026-100721- CVSS 4.0
- 9.5 Critical
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 9.0 Critical
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H - Weakness
- CWE-863
- Updated
- 2026-11-02
OSV
9.5
vm2 before 3.12.2 Authorization Bypass via Custom Resolver
vm2 before 3.12.2 contains an authorization bypass in the NodeVM external-module resolver. When an embedder configures require.external with a custom resolver (and context: 'host'), LegacyResolver.customResolve in lib/resolver-compat.js records the resolved module directory in this.externals as new RegExp('^' + escapeRegExp(resolvedPath)), without requiring a path separator or end-of-string boundary. Untrusted guest code can therefore require the allowlisted module (e.g. foo) and then require the absolute path of a non-allowlisted sibling whose path merely shares the resolved prefix (e.g. .../node_modules/foo2/index.js); the sibling passes isPathAllowedForModule and is loaded through hostRequire, so its top-level code runs in the host process before the exports are wrapped with vm.readonly, resulting in a sandbox escape and arbitrary code execution in the host context.
- Record
CVE-2026-100721- CVSS 4.0
- 9.5 Critical
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - Weakness
- CWE-863
- Updated
- 2026-11-02
Show 6 moreShow less
OSVGHSA-5h3f-q97h-ccvc
9.5
vm2: NodeVM custom resolution bypasses external path boundaries
Summary
At source revision 91034466bfb7f56b95fd48083ec6ca36d058f164 of vm2 3.11.8, an untrusted NodeVM guest can turn one allowlisted custom-resolved module into authorization for a separate file whose path merely shares the resolved path's string prefix. LegacyResolver.customResolve stores the resolved path in this.externals as ^<path> without an end or separator boundary; a later absolute require of a sibling such as foo2/index.js therefore passes the external check and is loaded through hostRequire when the configured context is host. The decisive attack loaded foo as FOO_OK and then executed the prefix-sharing sibling, which returned PREFIX_PWN after invoking child_process; an otherwise identical control denied the sibling with ENOTFOUND.
Technical Details
The affected configuration is a documented NodeVM use in which the embedder sets require.external to {modules: ['foo'], transitive: false}, supplies a custom resolver that returns the foo directory, sets a root directory, and uses context: 'host'. The guest controls the require specifiers and requests the allowlisted bare name before requesting the absolute path of the prefix-sharing sibling. The test entry point is deliberately outside the configured root so ordinary node_modules lookup misses and the custom resolver is consulted.
The source-to-sink path is:
NodeVM.runexecutes guest code and creates the module-specificrequirefunction atlib/nodevm.js:516-575.DefaultResolver.resolveFullsearches normal locations and then dispatches a miss tocustomResolveatlib/resolver.js:227-316.LegacyResolver.customResolvechecks the bare specifier against the external cache, calls the embedder's resolver, and appends an authorization regular expression atlib/resolver-compat.js:266-291.isPathAllowedForModulefalls back tothis.externals.some(regex => regex.test(path))atlib/resolver-compat.js:193-215. The dynamically appended expression^/…/node_modules/fooalso matches/…/node_modules/foo2/index.jsbecause no path separator or end-of-string condition is required.loadJSusesthis.hostRequire(filename)for a host-context file and only wraps the returned exports withvm.readonlyatlib/resolver-compat.js:255-263. Top-level effects of the required file therefore occur in the host process before the exports are wrapped.
The relevant current code has the same flaw for both supported custom-resolver return shapes:
if (typeof resolved === 'string') {
this.externals.push(new RegExp('^' + escapeRegExp(resolved)));
return this.loadAsFileOrDirectory(resolved, extList);
}
const {module=x, path: resolvedPath} = resolved;
this.externals.push(new RegExp('^' + escapeRegExp(resolvedPath)));
return this.loadNodeModules(module, [resolvedPath], extList);
The existing anchored bare-specifier matcher and the separator-aware mod.path check address different authorization states. They do not constrain the new this.externals entries created after custom resolution. The violated invariant is that a resolved allowlisted path may authorize only that exact path and its descendants after a path separator, never a sibling selected by raw string prefix.
PoV
The minimal guest operation is to load the configured bare name and then request the separate absolute sibling path:
const foo = require('foo');
module.exports = {
foo,
sibling: require('/tmp/vm2-prefix-case/node_modules/foo2/index.js')
};
The corresponding embedder configuration is:
const vm = new NodeVM({
require: {
external: {modules: ['foo'], transitive: false},
root: '/tmp/vm2-prefix-case',
context: 'host',
resolve(name) {
return name === 'foo'
? '/tmp/vm2-prefix-case/node_modules/foo'
: undefined;
}
}
});
The sibling is not itself allowlisted. Its successful load is the authorization violation; the child_process call in its top-level code demonstrates that the file ran in the…
- Record
GHSA-5h3f-q97h-ccvc- CVSS 4.0
- 9.5 Critical
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H - CVSS 3.1
- 10.0 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H - Weakness
- CWE-863
- Updated
- 2026-11-02
OSVGHSA-c9wr-qm7p-p6vc
9.5
Duplicate Advisory: vm2: NodeVM custom resolution bypasses external path boundaries
Duplicate Advisory
This advisory has been withdrawn because it is a duplicate of GHSA-5h3f-q97h-ccvc. This link is maintained to preserve external references.
Original Description
vm2 before 3.12.2 contains an authorization bypass in the NodeVM external-module resolver. When an embedder configures require.external with a custom resolver (and context: 'host'), LegacyResolver.customResolve in lib/resolver-compat.js records the resolved module directory in this.externals as new RegExp('^' + escapeRegExp(resolvedPath)), without requiring a path separator or end-of-string boundary. Untrusted guest code can therefore require the allowlisted module (e.g. foo) and then require the absolute path of a non-allowlisted sibling whose path merely shares the resolved prefix (e.g. .../node_modules/foo2/index.js); the sibling passes isPathAllowedForModule and is loaded through hostRequire, so its top-level code runs in the host process before the exports are wrapped with vm.readonly, resulting in a sandbox escape and arbitrary code execution in the host context.
- Record
GHSA-c9wr-qm7p-p6vc- CVSS 4.0
- 9.5 Critical
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X - CVSS 3.1
- 9.0 Critical
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H - Weakness
- CWE-863
- Updated
- 2026-11-02
OSVROOT-APP-NPM-CVE-2026-100721
10.0
CVE-2026-100721 in vm2 - Patched by Root
Root has patched CVE-2026-100721 in the vm2 package for Root:npm. Multiple fixed versions available.
- Record
ROOT-APP-NPM-CVE-2026-100721- CVSS 3.1
- 10.0 Critical
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H - Updated
- 2026-11-06
OSVCLSA-2026-1791887363 No score
TuxCare security update for vm2 (1 CVE)
TuxCare rebuilt vm2 to address 1 CVE. Fixed in 3.10.0-tuxcare.8.
- Record
CLSA-2026-1791887363- Updated
- 2026-11-02
OSVCLSA-2026-1791888008 No score
TuxCare security update for vm2 (1 CVE)
TuxCare rebuilt vm2 to address 1 CVE. Fixed in 3.11.7-tuxcare.3.
- Record
CLSA-2026-1791888008- Updated
- 2026-11-02
OSVCLSA-2026-1791888184 No score
TuxCare security update for vm2 (2 CVEs)
TuxCare rebuilt vm2 to address 2 CVEs. Fixed in 3.11.5-tuxcare.8.
- Record
CLSA-2026-1791888184- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
vm2npm |
through 3.12.1 | 3.12.2 |
vm2TuxCare:npm |
3.10.0-tuxcare.0 up to 3.10.0-tuxcare.8, plus listed versions 3.10.03.10.0-tuxcare.13.10.0-tuxcare.23.10.0-tuxcare.33.10.0-tuxcare.43.10.0-tuxcare.53.10.0-tuxcare.63.10.0-tuxcare.7 |
3.10.0-tuxcare.8 |
vm2TuxCare:npm |
3.11.7-tuxcare.0 up to 3.11.7-tuxcare.3, plus listed versions 3.11.73.11.7-tuxcare.13.11.7-tuxcare.2 |
3.11.7-tuxcare.3 |
vm2TuxCare:npm From an advisory covering 2 CVEs |
3.11.5-tuxcare.0 up to 3.11.5-tuxcare.8, plus listed versions 3.11.53.11.5-tuxcare.13.11.5-tuxcare.23.11.5-tuxcare.33.11.5-tuxcare.43.11.5-tuxcare.53.11.5-tuxcare.63.11.5-tuxcare.7 |
3.11.5-tuxcare.8 |
vm2npm |
before 3.12.2 | 3.12.2 |
vm2Root:npm |
before 3.11.6-aikido.1 | 3.11.6-aikido.13.11.8-aikido.1 |
@rootio/vm2Root:npm |
before 3.11.6-root.io.1 | 3.11.6-root.io.13.11.8-root.io.1 |
Public exploits
Links only. Every CVE stores no exploit code.
| Source | Link | Date |
|---|---|---|
| PoC in GitHub | murrez/CVE-2026-100721 | 2026-09-28 |
History
| Recorded | Changed | Sources |
|---|---|---|
| affected | OSV | |
| affected, facts | ||
| affected | ||
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV, PoC in GitHub |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-100721 is a critical vulnerability (CVSS 9.5) in Patriksimek Vm2, published September 27, 2026. Public exploit code exists. EPSS puts the chance of exploitation in the next 30 days at 0.59%, the 48th percentile. Fixed in vm2 3.12.2.
References
github.com/patriksimek/vm2/security/advisories/GHSA-5h3f-q97h-ccvc
github.com/patriksimek/vm2/commit/6ac3916da84e060c403e407b6b6318fcc66b0e72
github.com/patriksimek/vm2/commit/ab4ee7d803e8c80155e9eb3672226bddbca4aa9c
github.com/patriksimek/vm2/commit/91034466bfb7f56b95fd48083ec6ca36d058f164
vulncheck.com/advisories/vm2-before-3.12.2-authorization-bypass-via-custom-resolver
nvd.nist.gov/vuln/detail/CVE-2026-100721
Show 6 moreShow less
github.com/patriksimek/vm2/commit/fb4977f6211da6cadffdf1f3a2017a826a21c51f
github.com/patriksimek/vm2/releases/tag/v3.12.2
github.com/advisories/GHSA-5h3f-q97h-ccvc
github.com/advisories/GHSA-c9wr-qm7p-p6vc
github.com/CVEProject/cvelistV5/tree/main/cves/2026/100xxx/CVE-2026-100721.json
github.com/patriksimek/vm2
Left out here: the rest of 2 source descriptions. The JSON record has them.