GHSA-7q3f-wx44-378m: Path Traversal

Published Oct 1, 2026
·
Updated

Summary

isPathAllowedForModule decides whether a resolved path belongs to an allowlisted external module using a raw string prefix test. nodemodules/foo2 starts with nodemodules/foo, so a package whose name merely shares a prefix with an allowlisted one is treated as being inside it, and a relative require from the allowlisted package reaches it even with transitive loading disabled.

Where it is

lib/resolver-compat.js, lines 122 to 132, quoted from HEAD 7a1f5100b96f48d34e0fe104ab37c0acc5944f92:

js isPathAllowedForModule(path, mod) { if (!super.isPathAllowed(path)) return false; if (mod) { if (mod.allowTransitive) return true; if (path.startsWith(mod.path)) { const rem = path.slice(mod.path.length); if (!/(?:^|[\\/])nodemodules(?:$|[\\/])/.test(rem)) return true; } } return this.externals.some(regex => regex.test(path)); }

With mod.path of .../nodemodules/foo and a resolved path of .../nodemodules/foo2/index.js, startsWith is true and rem is 2/index.js, which contains no nodemodules segment, so the function returns true.

The nodemodules test in rem is what stops a genuine transitive dependency from slipping through. It does not stop a sibling, because a sibling's remainder never contains that segment.

Impact

Code running in NodeVM under an external module allowlist with transitive: false can reach a package that was not allowlisted, provided an allowlisted package performs a relative require to a prefix-sharing sibling.

Two preconditions are worth stating plainly rather than leaving implicit. The deployment must already have such a package layout, and an allowlisted package must have a reachable code path that does the relative require. This is not something the attacker creates; it is something they find. That narrows it considerably, and it is why I have not scored it higher.

Reachability

NodeVM.run at lib/nodevm.js:506 executes the script. require comes from createRequireForModule at lib/setup-node-sandbox.js:168-172 and reaches the resolver callback at lib/nodevm.js:380-384. LegacyResolver.resolveFull at lib/resolver-compat.js:145-160 sets currMod for direct requires, the relative specifier resolves through DefaultResolver.resolveFull and tryFile at lib/resolver.js:327-330, and the authorization decision lands on the function above.

Suggested fix

Require a separator after the prefix, so a sibling cannot match:

js if (path === mod.path || path.startsWith(mod.path + path.sep)) {

That is the same anchoring the rem regex already applies to nodemodules, applied one level earlier.

Affected Software

1 affected componentFixes available
npm/vm2<=3.11.6
3.11.7

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/vm2 to a version that resolves this vulnerability.

    Fixed in 3.11.7
  2. Compensating control

    Update isPathAllowedForModule so the allowlisted module path prefix must be followed by a path separator: use a boundary check equivalent to path.startsWith(mod.path + path.sep), rather than the raw path.startsWith(mod.path) test, preventing sibling packages such as node_modules/foo2 from matching allowlisted node_modules/foo.

Event History

Oct 1, 2026
Advisory Published
via GitHub·03:26 PM
Data Sourced
via GitHub·03:26 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Which configurations are exposed to this bypass?

Configurations that allowlist an external module and disable transitive loading are affected when another resolved package path shares the allowlisted module path as a raw prefix. For example, an allowlisted module at node_modules/foo can incorrectly allow code from node_modules/foo2.

2

What does an attacker need to exploit this behavior?

The resolved path must begin with the allowlisted module's path but refer to a different package whose name shares that prefix. A relative require originating from the allowlisted package can then reach that package even though transitive loading is disabled.

3

How can I identify a potentially affected setup?

Review external-module allowlists and compare each allowed module path or name with other installed package paths that begin with the same value. Pay particular attention to prefix pairs such as foo and foo2 where the second package is located alongside the allowlisted package under node_modules.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203