Skip to content

Build1 publisher3 min readPublished

vm2's prefix allowlist let one approved module load its unapproved siblings

vm2's maintainer patched a CVSS 9.5 flaw in 3.12.2 where the module allowlist matched an approved path as a bare prefix and cleared a neighboring package. With NodeVM's default host context, the unapproved sibling ran with full Node authority.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

Illustration accompanying vm2's prefix allowlist let one approved module load its unapproved siblings
Generated illustration

What happened

  • vm2's own maintainer disclosed the flaw in advisory GHSA-5h3f-q97h-ccvc on September 8 with a working proof-of-concept harness, and the patch landed the same day.
  • NVD published the record on September 27 with VulnCheck as the CNA, and still lists it 'Deferred' while the MITRE record shows it 'PUBLISHED.'
  • SSVC rates exploitation as 'poc' only, with no reports of use in the wild and no KEV listing the author could verify.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • decision Anyone running vm2 with a custom resolver has to move to 3.12.2, which records a boundary-matched base path so an approved module stops clearing its neighbors.
  • constraint VM hardening cannot help here, because the guest never breaks out; it asks for a file and the authorization layer says yes.
  • exposure The embedders exposed are the ones who followed the documented pattern, custom resolver plus allowlist, because that allowlist was a bare prefix match.
  • precedent Any allowlist that checks a prefix carries the same defect: without an end-of-path boundary, approving one path approves every path that starts with it.

NodeVM lets an embedder set `require.external` with a custom resolver; the guest names a module, the resolver decides where it lives, and vm2 records that answer so later requires can be checked against the allowlist [6]. The defect is one line in `lib/resolver-compat.js`. When the resolver returns a path, `LegacyResolver.customResolve` stores it as `this.externals = new RegExp('^' + escapeRegExp(resolvedPath));` [7]. The regex anchors the start of the string and nothing else: no trailing path separator, no end-of-string anchor [7].

Later, when guest code requires another module, `isPathAllowedForModule` runs that regex against the requested path [8]. It asks one thing: does this path start with a string I approved? An approved prefix then clears any path that begins with the same characters, even a path the resolver never resolved [8].

The maintainer's harness shows the shape. Approve a module `foo` at `/tmp/vm2-prefix-case/node_modules/foo`, install a package `foo2` beside it, then have the guest require the full path to `foo2/index.js` [9]. `foo2` was never allowlisted, but its path starts with `foo`'s recorded prefix, so the check passes [9]. Under NodeVM's default `context: 'host'`, the sibling loads through `hostRequire`, so `foo2`'s top-level code runs in the host process with full Node.js authority before `vm.readonly` wraps anything [10]. On the candidate run the harness prints `PREFIX_PWN` from a host-side `child_process` call; the control run, with no overlapping prefix, fails cleanly with `ENOTFOUND` [11].

Strictly speaking, nothing escaped the VM: the guest requested a file, and the authorization layer approved the request [1]. That makes this harder to defend against than a parser bug, since hardening the VM does nothing for a permission check that matches too broadly [1].

The scope is narrow, and the advisory says so. The bug needs a non-default configuration: `require.external` with a custom resolver function, and that configuration is the documented pattern for "guest may use these modules, nothing else" [13][15]. People write it precisely because it is supposed to be the security control [15]. Stock vm2 with no custom resolver never reaches `customResolve` this way [13]. The CVSS 4.0 vector encodes that condition as `AT:P`, attack requires present conditions, and CISA's SSVC marks it "automatable: no" [14]. The score is 9.5 under CVSS 4.0 and 9.0 under the older 3.1 scale [2].

The fix in 3.12.2 records a boundary-matched base path, so `foo` no longer authorizes `foo2` [12]. The same mistake lives in any allowlist built the same way: decide access by asking whether a path starts with an approved string, and you have approved its neighbors too [8].

One caveat on provenance. The dev.to analysis says its author did not reproduce the issue, and traces every detail to the NVD description, the maintainer's GHSA, and the 3.12.2 release notes, which it reports agree [16].

What to watch

  • Whether NVD lifts the 'Deferred' status and reconciles it with MITRE's PUBLISHED record.
  • Any move in SSVC from 'poc' toward in-the-wild use, or a KEV listing.
  • Whether downstream projects embedding vm2 with custom resolvers ship their own patched builds.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories