vm2 沙箱库惊曝「12连杀」:三个满分漏洞掀起腥风血雨

导语:这事儿吧,得从两边儿看——红队选手们估计已经在摩拳擦掌急着复现漏洞了,而蓝队兄弟则是连夜升级。不过说实话,这次 vm2 的漏洞,有点离谱。


一、事件概述

vm2 是 Node.js 生态里大名鼎鼎的沙箱库,专治那些”我要运行别人提交的代码”的场景。程序员们用它来隔离不受信任的脚本,防止恶意代码摸到系统底层。

但最近,安全研究人员给 vm2 来了一次彻底的”解剖”——一口气提交了 12 个安全报告,其中三个直接拿下 CVSS 10.0 满分,这是什么概念?这意味着这些漏洞在严重性评分表里已经是天花板级别了。

vm2沙箱逃逸概念图

具体来看这三个满分漏洞:

CVE-2026-47137——逻辑绕过。vm2 在 nodevm.js 里对 require 选项加了一道严格相等检查(=== false),但问题是你不传这个参数时值是 undefined,严格相等直接跳过。攻击者只需要不传 require,再利用 nesting: true 创建个不受限的子沙箱,就能直接跑系统命令了。

CVE-2026-47210——WebAssembly JSPI 逃逸。利用 WebAssembly 的 JSPI 模式和 Promise 机制,让宿主的 TypeError 构造函数泄露进攻击者可控的代码路径,从而拿到 process 对象执行任意命令。

CVE-2026-47140——拒绝列表绕过。process 模块和 inspector/promises 模块压根没被禁,攻击者可以调用 process.getBuiltinModule 重新加载被拉黑的 child_process,或者通过 Inspector 协议直接在宿主进程里跑 JS 代码。

受影响范围:vm2 所有版本直到 3.11.3,使用 Node.js 26 的系统尤其危险。


二、技术剖析:这三个满分漏洞是怎么实现的

2.1 CVE-2026-47137:检查时和使用时的”时空错位”

这个漏洞的本质是个经典的 TOCTOU(Time-of-check to Time-of-use)变体,只不过体现在参数校验上。

开发者在 3.11.3 之前的某个版本意识到:如果同时开启 nesting: true(允许创建子沙箱)但把 require 关掉(require: false),沙箱里的代码可以 require('vm2') 拿到库本身,然后构造一个配置更宽松的子沙箱实现完全逃逸。

所以作者在 nodevm.js 第 263 行加了一道关卡:

if (options.nesting === true && options.require === false) {
    throw new VMError('...');
}

但问题来了——=== false 太严格。只有在用户显式传入 require: false 时条件才成立。如果用户根本不传 require 选项,options.require 的值是 undefined,而 undefined === falsefalse,检查直接跳过。

然后第 280 行有个解构赋值的默认值:

const { require: requireOpts = false } = options;

运行时 requireOpts 被赋值为 false,行为等同于 require: false但安全检查已经被绕了

攻击代码长这样:

const nvm = new NodeVM({ nesting: true }); // 不传 require,绕过检查
nvm.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()"
    );
`);

内层 NodeVM 的配置完全独立于外层,没有任何”继承父沙箱限制”的机制。一旦你能控制内层沙箱配置,你就能给它任意权限。

漏洞利用流程图

2.2 CVE-2026-47210:WebAssembly JSPI 和 Promise 的”联姻”

这个漏洞利用了 WebAssembly JSPI(JavaScript Promise Integration)模式和 Promise 的 .finally() 方法。

攻击链是这样的:JSPI 支持的 Promise 从宿主进程创建,当这个 Promise 被 reject 时,产生的 TypeError 对象会流经攻击者可控的”物种构造函数”(species constructor)链条。通过精心构造的代码,攻击者能让 TypeError 的构造函数链指向宿主进程的构造函数,从而获取对 process 对象的引用。

说人话就是:攻击者找到了沙箱边界上的一个缝隙,让沙箱里的代码”窥视”到了宿主进程的对象体系,然后顺着这条线爬到了系统命令执行。

2.3 CVE-2026-47140:拒绝列表的”百密一疏”

vm2 有一个内置的拒绝列表(denylist),用来禁止某些危险模块比如 child_process。但安全研究人员发现,process 模块本身和 inspector/promises 模块压根没上名单。

这就有意思了:

  • 攻击者可以用 process.getBuiltinModule 重新加载被禁模块
  • 或者通过 Inspector 协议连接到宿主进程,在那里执行任意 JavaScript 代码

这属于典型的”你以为你禁了该禁的,但其实漏了几个”的问题。


三、影响范围与风险评估

根据 BeyondMachines 的披露,这次共发现 12 个 vm2 漏洞,多数为 CVSS 9.8 分及以上。其中 CVE-2026-44008CVE-2026-44009 目前暂无补丁。

风险场景主要包括:

  • 在线代码执行平台:允许用户提交 JavaScript 代码并运行的平台
  • 自动化测试环境:用 vm2 隔离测试用例
  • 插件系统:让第三方开发者编写插件并安全执行
  • 教育平台:在线编程学习平台的代码运行环境

如果你的应用用到了 NodeVM 或 VM,并配置了 nestingrequire 相关选项,请立即检查你的使用方式。


四、修复建议

immediate(立即行动)

  1. 升级到 vm2 3.11.4 或更高版本
  2. 如果无法立即升级,作为临时缓解措施:
  • 设置 require: false(显式设置)
  • 关闭 nesting 选项

长期方案

vm2 的沙箱逃逸漏洞已经不是第一次了。早在 2023 年就被曝出多个高危漏洞,这次又是”12 连杀”。从安全研究的角度看,沙箱本身就是一种”打补丁”式的防护思路,其安全性依赖于实现细节的复杂性。

如果你的场景对安全隔离有较高要求,建议评估更底层、更强壮的隔离方案,比如:

  • gVisor:Google 出的轻量级沙箱内核,运行在用户态
  • 轻量级虚拟机(MicroVM):如 Firecracker,基于硬件虚拟化
  • WebAssembly 运行时:如 Wasmtime、Wasmer,提供更细粒度的能力控制

五、总结

这事儿吧,vm2 的作者也是挺郁闷的——每次修完又被找到新洞。但换个角度想,能被这么深入地审计,其实也说明 vm2 在 Node.js 生态里的使用面足够广,值得安全社区花时间去研究。

对于正在用 vm2 的开发者来说,尽快升级到 3.11.4 是第一要务。如果暂时升级不了,至少把 nesting 关掉、require 显式设置为 false

最后提醒一句:沙箱从来不是银弹,它只是把攻击的门槛抬高了一点。对于那些真正需要跑不受信任代码的场景,还是建议上虚拟机级别的隔离方案。

版权声明:本文由华盟网原创发布,保留所有权利。配图由华盟网授权使用。

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容