MediumGHSA-r273-hxvj-fxhpCVE-2026-92933

vm2: util.getCallSites() bypasses GHSA-v27g-jcqj-v8rw host-frame redaction, leaks host call stack

Published 5 Oct 2026 · Updated 5 Oct 2026

Affected package

vm2npm
Affected
<3.11.8
Fixed in
3.11.8
Update to the fixed version for your release line, for example:npm install vm2@3.11.8

Details

Summary

NodeVM exposes the host util module to the sandbox through an unfiltered shallow copy (Object.assign({}, util)). On Node.js >= 22.9 this hands sandboxed code util.getCallSites(), a programmatic stack-introspection API that returns the host process's full call stack — absolute file paths, function names, and line numbers — including vm2 bridge internals and the embedding application's entrypoint. This bypasses the host-frame redaction established in GHSA-v27g-jcqj-v8rw, which only covers the Error.prepareStackTrace channel.

Details

  • Root cause — defaultBuiltinLoaderUtil copies every static member of the host util module and wraps the copy in vm.readonly() without filtering any member, so newly added Node APIs land in the sandbox automatically: https://github.com/patriksimek/vm2/blob/7a1f5100b96f48d34e0fe104ab37c0acc5944f92/lib/builtin.js#L25-L38
  • Second equivalent channel — the deprecated sys builtin (an alias of host util) goes through the generic builtin loader vm.readonly(hostRequire(key)), which also carries getCallSites: https://github.com/patriksimek/vm2/blob/7a1f5100b96f48d34e0fe104ab37c0acc5944f92/lib/builtin.js#L230
  • Bypassed protection — GHSA-v27g's redaction (isHostFrameFileName + applyCallSiteGetters) rewrites host-frame metadata getters to null only when the *sandbox realm* formats an error stack: https://github.com/patriksimek/vm2/blob/7a1f5100b96f48d34e0fe104ab37c0acc5944f92/lib/setup-sandbox.js#L818-L870

util.getCallSites() produces its data host-side and never passes through that formatter, so frames such as lib/bridge.js @apply (the bridge apply trap), lib/nodevm.js @run, the embedder's own entry file, and node:internal/* frames reach the sandbox verbatim as data properties (scriptName, functionName, lineNumber, columnNumber, scriptId). A repository-wide grep (source, docs/ATTACKS.md, CHANGELOG, tests) shows no occurrence of getCallSites; the member was never considered.

PoC

Prerequisites: Node.js >= 22.9 (verified on v22.23.1); run npm ci --no-audit --no-fund at the repository root.

One-line reproducer (prints /workspace/repo/lib/bridge.js, i.e. a vm2-internal host path):

node -e "const {NodeVM}=require('/workspace/repo'); console.log(new NodeVM({require:{builtin:['util']}}).run(\"module.exports = require('util').getCallSites(4)[0].scriptName\"))"

Observed frames inside the sandbox, with both require: { builtin: ['util'] } and require: { builtin: ['*'] }:

/workspace/repo/lib/bridge.js @apply:1664 vm.js @:5 /workspace/repo/lib/bridge.js @apply:1664 /workspace/repo/lib/nodevm.js @run:563 /workspace/out/<embedder entrypoint>.js @main:29 node:internal/modules/cjs/loader @:1781 node:internal/modules/cjs/loader @:1913 ... (8 node:internal/* frames in total)

Impact

Information disclosure. Any NodeVM configuration that allows the util (or sys) builtin — including the wildcard builtin: ['*'], both typical configurations from the README — lets untrusted sandboxed code programmatically read the host call stack: the vm2 installation path, the embedding application's file layout and entrypoint, and internal function names and line numbers. This is the same information category redacted by GHSA-v27g-jcqj-v8rw (Defense Invariant #5 in docs/ATTACKS.md) and breaks defense-in-depth assumptions of embedders that rely on host frames being invisible to the sandbox. The API returns only strings/numbers (no host object references); no escalation to code execution was identified.

Suggested fix: filter members in defaultBuiltinLoaderUtil (allowlist, or at minimum drop getCallSites), apply the same treatment to the generic loader path used by sys, and extend the GHSA-v27g regression tests to cover programmatic stack-introspection APIs.

Is GHSA-r273-hxvj-fxhp in any of your client repos?

Checking one package by hand is fine. Checking it across forty client projects is not. NPMscan reads the lockfiles in every GitHub repo you pick, finds each project with an affected vm2, and gives you the command that fixes it.

References

Advisory data from OSV.dev (opens OSV.dev in a new tab), refreshed daily.