Every CVE Search/ Get API key

CVE-2026-100721

vm2 before 3.12.2 Authorization Bypass via Custom Resolver

9.5 CriticalPublic exploitFix available

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

0.59%Up 0.2 points since Sep 27, 2026
90%50%10%1%

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 CriticalCVSS: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 CriticalCVSS: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 CriticalCVSS: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.run executes guest code and creates the module-specific require function at lib/nodevm.js:516-575.
  • DefaultResolver.resolveFull searches normal locations and then dispatches a miss to customResolve at lib/resolver.js:227-316.
  • LegacyResolver.customResolve checks the bare specifier against the external cache, calls the embedder's resolver, and appends an authorization regular expression at lib/resolver-compat.js:266-291.
  • isPathAllowedForModule falls back to this.externals.some(regex => regex.test(path)) at lib/resolver-compat.js:193-215. The dynamically appended expression ^/…/node_modules/foo also matches /…/node_modules/foo2/index.js because no path separator or end-of-string condition is required.
  • loadJS uses this.hostRequire(filename) for a host-context file and only wraps the returned exports with vm.readonly at lib/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 CriticalCVSS: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 CriticalCVSS: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 CriticalCVSS: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 CriticalCVSS: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 CriticalCVSS: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 CriticalCVSS: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 CriticalCVSS: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.run executes guest code and creates the module-specific require function at lib/nodevm.js:516-575.
  • DefaultResolver.resolveFull searches normal locations and then dispatches a miss to customResolve at lib/resolver.js:227-316.
  • LegacyResolver.customResolve checks the bare specifier against the external cache, calls the embedder's resolver, and appends an authorization regular expression at lib/resolver-compat.js:266-291.
  • isPathAllowedForModule falls back to this.externals.some(regex => regex.test(path)) at lib/resolver-compat.js:193-215. The dynamically appended expression ^/…/node_modules/foo also matches /…/node_modules/foo2/index.js because no path separator or end-of-string condition is required.
  • loadJS uses this.hostRequire(filename) for a host-context file and only wraps the returned exports with vm.readonly at lib/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 CriticalCVSS: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 CriticalCVSS: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 CriticalCVSS: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 CriticalCVSS: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 CriticalCVSS: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

Affected packages
PackageAffectedFixed
vm2
npm
through 3.12.1
3.12.2
vm2
TuxCare: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
vm2
TuxCare: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
vm2
TuxCare: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
vm2
npm
before 3.12.2
3.12.2
vm2
Root:npm
before 3.11.6-aikido.1
3.11.6-aikido.13.11.8-aikido.1
@rootio/vm2
Root: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.

Public exploits
SourceLinkDate
PoC in GitHubmurrez/CVE-2026-1007212026-09-28

History

History
RecordedChangedSources
affectedOSV
affected, facts
affected
first indexedCVE 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

Show 6 moreShow less
This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open