GHSA-7q3f-wx44-378m

Suggest an improvement
Source
https://github.com/advisories/GHSA-7q3f-wx44-378m
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-7q3f-wx44-378m/GHSA-7q3f-wx44-378m.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-7q3f-wx44-378m
Aliases
Downstream
Published
2026-10-01T15:26:45Z
Modified
2026-10-01T15:45:05Z
Severity
  • 4.2 (Medium) CVSS_V3 - CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N CVSS Calculator
Summary
vm2: External module allowlist uses a raw prefix test, so a prefix-sharing sibling package is treated as allowlisted
Details

Summary

isPathAllowedForModule decides whether a resolved path belongs to an allowlisted external module using a raw string prefix test. node_modules/foo2 starts with node_modules/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:

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 (!/(?:^|[\\/])node_modules(?:$|[\\/])/.test(rem)) return true;
        }
    }
    return this.externals.some(regex => regex.test(path));
}

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

The node_modules 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:

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

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

Database specific
{
    "cwe_ids":  [
        "CWE-22",
        "CWE-863"
    ],
    "github_reviewed":  true,
    "github_reviewed_at":  "2026-10-01T15:26:45Z",
    "nvd_published_at":  null,
    "severity":  "MODERATE"
}
References

Affected packages

npm / vm2

Package

Affected ranges

Type
SEMVER
Events
Introduced
0 Unknown introduced version / All previous versions are affected
Fixed
3.11.7

Database specific

last_known_affected_version_range
"<= 3.11.6"
source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/10/GHSA-7q3f-wx44-378m/GHSA-7q3f-wx44-378m.json"