◆ 安全研究解读 · RUBRIK ZERO LABS

劫持 n8n 的 HITL 聊天会话
—— 拆解 AI 编排平台的信任边界

一个未授权漏洞如何让攻击者窃听并劫持正在运行的「人在回路」AI 智能体对话,以及一次巧用 Python match/case 语法逃逸沙箱的精彩实战。

原文作者 Ori Lahav 发布 2026-07-08 系列 Breaking AI Orchestration · Part 2/3 来源 Rubrik Zero Labs Blog ↗
0
利用所需的身份凭据
3
相互叠加的复合缺陷
6.3
CVE-2026-42228 · 中危
2
条独立攻击路径

n8n 是当今最流行的 AI 编排平台之一,被大量大型组织用于快速搭建 AI 应用与自动化流程。本研究揭示了两条关键攻击路径:一是无需身份认证即可劫持实时聊天会话,二是逃逸 n8n Cloud 的 Python 沙箱。两者共同暴露出「拖拽式 AI 编排」在便捷背后被忽视的信任边界问题。

◆ 一句话总结 任何使用 n8n Chat 节点承载「人在回路」(Human-in-the-Loop, HITL) 智能体工作流的实例,其正在进行的对话都可被网络上的未授权攻击者发现、窃听并注入消息——因为会话标识可被枚举,且会话未与任何用户账户绑定。

攻击面测绘 · Mapping the Attack Surface

当类型系统与中间件各说各话

n8n 是一个 NestJS 风格的 TypeScript 应用,路由通过 @RestController、@Post、@Get 等装饰器声明,认证模型集中在单一的中间件链上——这本身是良好的设计。认证中间件只在 AbstractServer 中被装配一次,装饰器路由只有显式声明 skipAuth: true 才会跳过认证。

研究者对认证逻辑本身的审计结论是:核心稳固,JWT 使用规范。真正的问题不在装饰器体系内,而在它之外。对于这类应用,一条路由会「意外未授权」通常有两种成因:

A
装饰器体系内的类型/标志错配

研究者发现一个 setup 端点被标注为 AuthenticatedRequest,却又声明了 skipAuth: true——类型系统与中间件链自相矛盾。这正是 TypeScript 应用里最值得警惕的漏洞形态。此处因处理函数并未真正依赖 req.user,只是笔误而非漏洞,但是一个值得在全代码库 grep 的模式。

B
装饰器体系之外直接注册的路由

AbstractServer 还用 app.use(...) 与 app.all(...) 直接注册了一批路由,它们完全绕过 skipAuth / createAuthMiddleware 管线。每条这样的路由要么自带认证,要么就是有意公开。真正致命的那一条,注册代码里没有任何中间件、没有认证依赖、没有任何检查——「谁来请求,就给谁开一个聊天会话」。

◆ 审计启示 集中式认证是好设计,但「例外注册通道」(app.use / app.all)是任何框架里最容易失守的盲区。审计时不能只看装饰器路由,更要枚举所有绕过标准管线的注册点。

未授权劫持实时聊天会话

头号发现:窃听并接管 AI 智能体对话

凡使用 Chat 节点承载 HITL 智能体工作流、与真人保持实时对话的用户,都暴露给了一个未授权攻击者——对方既能窃听对话,也能注入消息。而由于标识可枚举,实例上每一个正在运行的 HITL 会话都能被发现。

Chat 节点是什么

n8n 的 Chat 节点让智能体工作流能与用户来回对话,通常作为 AI 智能体面向用户的一侧:发消息 → 等待回复 → 再发消息。底层上,「等待用户下一条消息」会挂起工作流执行;用户一回复,执行就从挂起处恢复。这一来一回的传输通道,是一个服务于 /chat 的 WebSocket。前端连接时会带上两个关键参数:

chat-websocket.url
// executionId:标识被挂起的工作流运行(可枚举整数)
// sessionId :该运行内部的聊天会话(客户端说了算)
wss://<host>/chat?executionId=1024&sessionId="abc..."

executionId 指向被挂起的工作流运行,sessionId 指向该运行内部的具体聊天会话;两者共同决定这个 socket 连接到哪一场对话。

三重复合缺陷:各自看似无害,叠加即致命

1
/chat 完全没有认证

如前所述,它的注册就是一行 app.use('/chat', ...)。因为在装饰器体系之外,所以没有 skipAuth 标志;而处理函数内部也没有任何手动认证检查。

2
executionId 是可枚举的自增整数

n8n 使用顺序自增的整数执行 ID。打开 WebSocket 并不要求你预先知道一个有效 ID——直接在循环里挨个尝试即可。

3
sessionId 由客户端任意指定

服务器不会把会话绑定到某个用户账户。就聊天层而言,谁先用某个 sessionId 连上,谁就是这个会话。

攻击链时序(点击每一步展开)

1
枚举执行 ID
for id in range(...) → probe
▸

攻击者从网络侧遍历自增整数执行 ID。无需任何凭据,也无需预知有效 ID——因为 ID 顺序可预测,且开连接不做任何校验。

2
用 oracle 判定「谁在等输入」
GET /form-waiting/:id/:suffix → "waiting"
▸

兄弟路由 /form-waiting/:executionId/:suffix(经同一 app.all(...) 块注册)在执行被挂起、等待输入时返回字面量字符串 "waiting",否则返回别的。它本身并不「坏」——设计用途是让轮询客户端检查表单是否完成。但从攻击者视角,它向未授权网络泄露了工作流执行状态,精确回答了「该攻击哪个执行、何时下手」。

要蹲守新会话,只需监视比当前最大 ID 再高一位的那个 ID 即可。

3
WebSocket 接入活跃会话
wss://host/chat?executionId=N&sessionId=...
▸

拿到一个活跃的 executionId 后,打开 WebSocket 即可直接附着到该会话。socket 是双向的,线上协议在代码里有据可查——JSON 格式的用户消息帧:

wire-protocol.json
{ "sessionId": "...",
  "action": "sendMessage",
  "chatInput": "任意攻击者可控内容" }
4
窃听 · 注入 · 重放
read output / inject message / replay turns
▸

视对面工作流而定,攻击者此时可以:

▹ 读取智能体输出——包括即将回给真实用户的工具执行结果、摘要或检索到的上下文;
▹ 注入新的用户消息以操纵、引导对话走向;
▹ 重放先前轮次——聊天历史会向新连接暴露。

CVE-2026-42228 Severity: Moderate · 6.3 该漏洞已被 n8n 团队修复。研究者提供了官方演示视频,可见合法用户经 UI 与 AI 智能体交互的同时,攻击者枚举 ID、读取会话消息、并自行注入消息以控制会话。

逃逸 n8n 的 Python 沙箱

当 AST 分析器不认识 match/case

n8n 允许用户输入 Python 代码以实现自定义处理逻辑。为安全地开放这一能力,用户代码运行在加固过的解释器中,防护由两道互补机制构成:

①
静态分析器:禁止危险模式

代码先经 SecurityValidator 做静态代码分析,拦截禁用模式——包括所有 import,以及 eval / exec / compile / __import__ / __globals__ / __class__ / __subclasses__ 等敏感名称。

②
受限执行:阉割的 builtins

代码经 Python 的 exec 执行,全局命名空间仅含 print 函数及一个受限的 __builtins__ 子集。禁用名单还包含 eval, exec, compile, open, getattr, object, type, globals 等等。

◆ 攻击核心思路 两道防护都会在攻击者令某个变量指向原始的、未过滤的 globals 时崩塌。常规思路是通过尚存的 print 拿到 print.__builtins__["exec"]——但访问 __builtins__ 成员(无论用 . 点号还是 getattr)已被静态分析拦截。

突破口:结构化模式匹配 match/case

研究发现的绕过手法,利用了一个分析器当时未建模的 Python 特性——结构化模式匹配。match/case 在类模式层面通过结构匹配来绑定属性,而对应的 AST 节点类型 MatchClass 根本不在访问器(visitor)的遍历路径上,于是其中的关键字属性溜过了所有其他检查。

sandbox-escape.py · 概念演示
# 前置:先拿到一个 object 引用(object 本身在禁用名单上)
# 技巧:任何具体类的内建方法的类型元数据上都挂着 object
obj = list.__format__.__objclass__   # → <class 'object'>

# 用 match 的类模式绑定,MatchClass 节点未被 visitor 访问
match obj():
    case object(__builtins__=builtins):   # 关键字属性溜过检查
        # builtins 即为「未过滤」的原始 builtins
        real_exec = builtins["exec"]
        real_exec("import os; print(os.listdir('/'))",
                  print.__globals__)   # 借 print.__globals__ 作真实模块 globals

list.__format__ 是一个内建方法,其 __objclass__ 返回定义该方法的类——对 __format__ 而言正是 object。拿到 object 后,match 模式便把未过滤的 __builtins__ 直接绑进本地变量;再从中取出 eval / exec,并以 print.__globals__ 作为真实的模块 globals 字典喂给 exec,即可执行任意代码。完整 payload 粘进 Code 节点后,成功返回了根目录的文件列表。在 n8n Cloud 上实测——有效。

◆ 一个耐人寻味的尾声 研究者写报告前复测,发现漏洞已失效:分析器新增了 visit_MatchClass 方法,遍历每个模式的关键字属性并拦截禁用名称,恰好堵住了这个洞。究竟是团队察觉了测试、还是另有研究者先行上报,不得而知。

◆ 沙箱设计的真正教训

  • 教训不是「用黑名单」。而是:静态分析器的能力上限,等于它所访问的 AST 节点类型集合,以及它所认识的模式集合。
  • Python 在不断新增绑定或解引用属性的语法形式——match/case 是其一,未来的语言特性还会不断制造类似缺口。
  • 基于 AST 分析的沙箱,每当语法(grammar)增长,就必须重新审计。对 Python 代码做全局过滤与静态分析,单靠自身永远不够。
  • 这正是 n8n 除上述机制外,还支持将代码运行在独立 task runner 中的原因——应当启用它,才能获得可靠的安全边界。

防御自检清单 · Hardening Checklist

面向运维与安全团队的行动项

勾选下列条目以跟踪你的加固进度。以下建议综合自本研究的技术结论,请结合自身部署环境评估。

加固进度
0%
升级到已修复版本确保覆盖 CVE-2026-42228 的补丁(会话认证/绑定修复)与 visit_MatchClass 沙箱修复。
收敛网络暴露面切勿将 n8n 实例(尤其 /chat、/form-waiting 端点)直接暴露到公网;置于反向代理、VPN 或零信任网关之后。
启用独立 Task Runner 执行 Python不要仅依赖静态分析+受限 builtins;将 Code 节点代码运行在隔离的 task runner 中,构建真正的进程/容器级安全边界。
审计「装饰器体系之外」的路由自研/二开时,枚举所有 app.use / app.all 直接注册的路由,确认每条都自带认证或确属有意公开。
grep 类型/标志错配在 TypeScript 代码库中排查「AuthenticatedRequest 类型 + skipAuth:true」这类自相矛盾的声明。
避免可枚举的顺序标识对会话/执行标识使用不可枚举的随机值(如 UUID),并将会话严格绑定到已认证的用户身份。
随语言版本重审 AST 沙箱每当 Python 语法增长,重新审计静态分析器的 visitor 覆盖范围,确保新增节点类型被纳入检查。

披露时间线 · Disclosure Timeline

2026-02-08
向 n8n 团队上报漏洞
2026-04-20
漏洞修复完成
2026-07-08
Rubrik Zero Labs 公开披露(本文);研究曾于 BlueHat IL 2026 以《Exploiting AI Orchestration Zero-Days via PostgreSQL Internals》为题发表

核心启示 · Key Takeaways

◆ 三条贯穿全文的主线

  • 复合缺陷的杀伤力——「无认证的 /chat」「可枚举的 executionId」「客户端可控的 sessionId」单看都不惊人,叠加即构成可靠、可规模化的未授权劫持。安全评审必须看组合,而非孤立缺陷。
  • 「设计良好」不等于「没有例外」——n8n 的集中式认证是优秀设计,但真正失守的是绕过标准管线的旁路注册。任何框架的例外通道都值得被单独、彻底地审计。
  • AST 沙箱是移动的靶子——静态分析永远滞后于语言的语法演进。match/case 只是这一类缺口的当下样本;真正的安全边界应建立在进程/容器隔离(独立 task runner)之上。