GHSA-7Q3F-WX44-378M

Vulnerability from github – Published: 2026-10-01 15:26 – Updated: 2026-10-01 15:26
VLAI
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.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 3.11.6"
      },
      "package": {
        "ecosystem": "npm",
        "name": "vm2"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "3.11.7"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-92945"
  ],
  "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"
  },
  "details": "## Summary\n\n`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.\n\n## Where it is\n\n`lib/resolver-compat.js`, lines 122 to 132, quoted from HEAD `7a1f5100b96f48d34e0fe104ab37c0acc5944f92`:\n\n```js\nisPathAllowedForModule(path, mod) {\n    if (!super.isPathAllowed(path)) return false;\n    if (mod) {\n        if (mod.allowTransitive) return true;\n        if (path.startsWith(mod.path)) {\n            const rem = path.slice(mod.path.length);\n            if (!/(?:^|[\\\\/])node_modules(?:$|[\\\\/])/.test(rem)) return true;\n        }\n    }\n    return this.externals.some(regex =\u003e regex.test(path));\n}\n```\n\nWith `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.\n\nThe `node_modules` test in `rem` is what stops a genuine transitive dependency from slipping through. It does not stop a sibling, because a sibling\u0027s remainder never contains that segment.\n\n## Impact\n\nCode 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.\n\nTwo 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.\n\n## Reachability\n\n`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.\n\n## Suggested fix\n\nRequire a separator after the prefix, so a sibling cannot match:\n\n```js\nif (path === mod.path || path.startsWith(mod.path + path.sep)) {\n```\n\nThat is the same anchoring the `rem` regex already applies to `node_modules`, applied one level earlier.",
  "id": "GHSA-7q3f-wx44-378m",
  "modified": "2026-10-01T15:26:45Z",
  "published": "2026-10-01T15:26:45Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/patriksimek/vm2/security/advisories/GHSA-7q3f-wx44-378m"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-92945"
    },
    {
      "type": "WEB",
      "url": "https://github.com/patriksimek/vm2/commit/6ac3916da84e060c403e407b6b6318fcc66b0e72"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/patriksimek/vm2"
    },
    {
      "type": "WEB",
      "url": "https://github.com/patriksimek/vm2/releases/tag/v3.11.7"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/vm2-before-3.11.7-module-allowlist-bypass-via-prefix-matching"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "vm2: External module allowlist uses a raw prefix test, so a prefix-sharing sibling package is treated as allowlisted"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…