vm2 的 nesting 防護被空陣列繞過,require: [] 即可 RCE
GitHub Advisory Database · GHSA-8hr7-r645-pc6w / CVE-2026-92935 · GitHub 審核發布於 2026-10-01
vm2 的 NodeVM 在 nesting: true 時,原本要求開發者必須明確給一份 require 設定才放行,這道檢查只用 typeof === 'object' 判斷,所以 require: [] 這種空陣列就能通過,沙箱內的程式碼隨即取得宿主端的 vm2,再建出一個自選 builtin 白名單的內層 NodeVM,最後以宿主身分執行指令。
這是對 GHSA-m4wx-m65x-ghrr(CVE-2026-47137)修補的繞過,影響 npm vm2 >= 3.11.4, <= 3.11.6,修在 3.11.7。GitHub 給的 CVSS v3 分數為 9.0,嚴重度 critical。
原本的問題
這個洞的根源是「補丁只擋了已知的壞輸入」。上一輪 GHSA-m4wx-m65x-ghrr 的原始漏洞是:省略 require 時,預設值被解構成 false,等於 nesting: true 加 require: false,而護欄用嚴格相等只比對了 false。那次修補把護欄改成「必須是物件」,卻沒有問「這個物件真的是設定嗎」。
於是同一條攻擊鏈換了一種輸入形狀又走通:護欄放行、解構成全空欄位、resolver 只剩 vm2。commit 的說明把這類情況歸為 typeof 為 object 的非設定值,也就是第四種會讓 resolver 退化成只有 NESTING_OVERRIDE 的輸入。這也是為什麼修補選擇用「只認 plain object」的白名單,而不是再多列幾種要擋的型別。
漏洞機制
nesting 是 vm2 刻意保留的逃生口:開啟後,沙箱可以 require('vm2') 自己再建巢狀的 VM。修補 commit 的說明指出,這個 NESTING_OVERRIDE 會無條件暴露宿主 vm2,所以 GHSA-m4wx-m65x-ghrr 加了一道護欄:只要 nesting 為真,require 就必須是真正的設定物件或 Resolver,否則在建構時丟 VMError。
護欄的實作是這一句:
hasRealRequireConfig =
requireOpts instanceof Resolver ||
(typeof requireOpts === 'object' && requireOpts !== null);問題在於 陣列、Date、RegExp、Map、Set、typed array、boxed primitive 的 typeof 全都是 'object'。這些值通過護欄後,makeResolverFromLegacyOptions() 對它做物件解構,builtin、external 等欄位全是 undefined,再併入 NESTING_OVERRIDE.vm2,得到的 resolver 只暴露宿主 vm2,沒有任何限制。
外層設了什麼 builtin 限制都不重要,因為內層 NodeVM 的 require 設定是由沙箱內的程式碼自己決定的。
受影響版本
vm2>= 3.11.4, <= 3.11.6,已修於3.11.7。- 前提有兩個:宿主以真值
nesting與陣列形狀的require建立NodeVM,而且攻擊者能提供被該NodeVM執行的 JavaScript。 - advisory 特別提到,舊的
GHSA-m4wx-m65x-ghrr把3.11.4標成已修補,這個說法並不正確。只升到3.11.4的專案仍然暴露。
advisory 附的 PoC 如下,外層用 {nesting: true, require: []},沙箱內部三行就跑出 id 的輸出:
const outer = new NodeVM({nesting: true, require: []});
outer.run(`
const {NodeVM} = require('vm2');
const inner = new NodeVM({require: {builtin: ['child_process']}});
module.exports = inner.run(
"module.exports = require('child_process').execSync('id').toString()");
`);修補與緩解
3.11.7 的修補分兩層。第一層在 lib/nodevm.js:hasRealRequireConfig 改用新的 isPlainConfigObject(),只接受原型為 Object.prototype 或 null 的物件,並先用 Array.isArray 排除陣列,連原型被 Proxy 偽裝的陣列也擋得住。
第二層在 makeResolverFromLegacyOptions():當 override 存在而 options 不是 plain object 時,直接把 override 清成 undefined。即使有別的呼叫路徑繞過第一層,也不會再把宿主 vm2 注入非設定值。
| 設定 | 3.11.6 以前 | 3.11.7 |
|---|---|---|
{nesting: true, require: []} | 通過護欄,暴露宿主 vm2 | 建構時丟 VMError |
{nesting: true, require: {}} | 可用 | 仍可用(文件化的逃生口) |
{nesting: false, require: []} | 可用 | 不受影響 |
該 commit 同時附上 test/ghsa/GHSA-8hr7-r645-pc6w/ 的 repro.js 與 adversarial.js,涵蓋陣列、Date、RegExp、Map、Set、typed array、boxed primitive 與 Proxy 偽裝等變體。v3.11.7 release 於 2026-08-24 發布,release notes 稱這是 patch release、沒有 API 變更,並一併關閉 GHSA-647f-g98j-qq25 等其他 advisory。
影響範圍
會被打到的是把 vm2 當外掛或使用者腳本沙箱、又開了 nesting 的服務,例如 workflow 引擎、低程式碼平台、線上程式碼執行器。先 grep 專案裡所有 new NodeVM(,找出 nesting 為真的呼叫,再確認 require 是不是從設定檔、環境變數或 JSON 反序列化來的;從 JSON 讀進來的空陣列正是這個漏洞的典型入口。
處理順序:先用 npm ls vm2 確認解析到的版本,在 >= 3.11.4, <= 3.11.6 就升到 3.11.7 以上。短期無法升級時,advisory 的說明是只有同時啟用 nesting 且傳入陣列形 require 的應用才會中招,所以最直接的緩解是關掉 nesting,或把 require 改成明確的 plain object。