n8n表达式注入漏洞CVE-2025-68613:从原理到RCE的深度剖析与防御

1. 项目概述:一个被低估的自动化工具高危漏洞

最近在安全圈里,一个关于n8n的漏洞编号CVE-2025-68613引起了我的注意。n8n,这个在开发者圈子里越来越火的低代码/无代码工作流自动化工具,竟然爆出了一个表达式注入漏洞,并且能一路打到远程代码执行。这可不是小事。很多团队,包括一些初创公司和内部效率工具团队,都喜欢用n8n来串联各种API、处理数据,因为它图形化界面友好,节点丰富。但大家往往容易忽略,这种“拖拉拽”就能构建复杂逻辑的背后,其表达式引擎的安全性直接关系到整个系统的命脉。

这个漏洞的核心,简单来说,就是攻击者能够向n8n工作流中某些本应只处理数据的节点,注入恶意的JavaScript表达式。由于n8n底层基于Node.js,这些表达式在特定上下文(沙箱逃逸后)中被执行,最终可能导致攻击者在服务器上运行任意命令,也就是我们常说的RCE。我之所以花时间深入研究它,是因为这类漏洞的利用链非常典型:从应用层一个看似“功能点”的输入,穿透层层逻辑,最终抵达系统底层。理解它,不仅能帮助我们加固n8n,更能举一反三,审视其他基于表达式或模板引擎的Node.js应用。

对于安全研究人员、DevOps工程师、以及任何在生产环境部署了n8n的团队来说,理解CVE-2025-68613的来龙去脉至关重要。这不仅仅是打一个补丁那么简单,更是重新审视“低代码平台安全性”的契机。接下来,我会带你从漏洞原理、环境搭建、手工复现,一路讲到深度利用和立体防御,把这条攻击链掰开揉碎了讲清楚。

2. 漏洞原理深度拆解:表达式注入如何演变为RCE

要理解CVE-2025-68613,我们不能把它看成一个孤立的点,而是一条有逻辑递进关系的攻击链。这条链大致可以分为三个关键阶段:入口点的寻找与利用表达式沙箱的突破、以及最终RCE的达成。我们一层层来看。

2.1 n8n表达式引擎与漏洞入口点

n8n的强大之处在于其表达式引擎。在几乎每个节点中,你都可以看到一个小的“fx”按钮,点击后可以输入类似{{ $json.itemId }}{{ $now.format('YYYY-MM-DD') }}这样的表达式。这些表达式在节点执行时会被动态求值,从而允许工作流实现动态逻辑。引擎底层使用了类似vm2或自定义的沙箱环境来隔离这些表达式,防止它们访问危险的全局对象如requireprocess

漏洞的入口,就藏在一些接收并处理用户输入,且其输出会被后续节点“表达式化”的地方。一个典型的场景是“HTTP Request”节点。假设一个工作流设计是:接收用户通过HTTP POST提交的JSON数据,然后通过一个“Function”节点或“Set”节点,将数据中的某个字段(如userInput)直接拼接到一个表达式字符串中,并交给表达式引擎去执行。

例如,一个危险的设计可能是:

// 在Function节点中,危险地将用户输入拼接进表达式 const userData = $input.first().json.userInput; // 假设用户输入是 `}}; console.log(process.mainModule); //` const dynamicExpression = `{{ ${userData} }}`; // 然后试图将这个dynamicExpression在另一个上下文中求值

如果n8n对userInput的净化不彻底,攻击者输入的闭合符}}可以提前结束原有的表达式上下文,并注入自己的JavaScript代码。但关键在于,早期的n8n版本中,某些路径下对这类输入的过滤存在缺陷,或者在某些节点配置(如“Code”节点的旧版本模式)中,提供了更直接的代码执行能力,为注入打开了窗口。

2.2 从表达式注入到沙箱逃逸

即使成功注入了JS代码,这些代码默认也应在n8n构建的沙箱中运行。n8n使用的沙箱旨在限制对Node.js核心模块和宿主环境的访问。然而,沙箱逃逸是这类漏洞的经典环节。攻击者会利用JavaScript原型链污染、沙箱内置对象的方法暴露、或者上下文传递中的缺陷,来获取一个“跳出”沙箱的桥梁。

在CVE-2025-68613相关的利用链中,攻击者可能利用了沙箱环境对外部某些特定函数或对象的引用。例如,通过this.constructor.constructor这样的原型链技巧,有可能获取到外层“非沙箱”环境的Function构造函数,从而构造一个可以执行任意代码的函数。又或者,发现沙箱内可以通过某些特殊路径访问到process对象或require函数。一旦逃逸成功,攻击者就获得了在Node.js应用主进程上下文执行代码的能力,这与直接在应用代码中写eval()几乎无异。

注意:具体的沙箱逃逸向量(Payload)会因n8n的具体版本和配置而异。在漏洞公开初期,利用代码往往依赖于某个未公开的细节。这也是为什么及时更新到已修复版本至关重要。

2.3 实现远程代码执行的关键跳板

沙箱逃逸后,攻击者执行的代码仍在Node.js的V8引擎进程中。要实现真正的RCE(例如执行系统命令lswhoami或反弹shell),就需要调用Node.js的child_process模块。由于逃逸后已处于主进程上下文,直接使用require('child_process')成为可能。

一个典型的最终Payload结构可能是这样的:

// 假设这是成功注入并逃逸后执行的代码 const { execSync } = require('child_process'); const output = execSync('id').toString(); // 然后需要将结果输出或外传,例如通过另一个HTTP请求发送到攻击者服务器

至此,从一次看似普通的数据提交,到最终在服务器上执行任意命令的完整攻击链路就打通了。其危害等级之所以是“高危”甚至“严重”,正是因为这条链路在默认或常见配置下是可实现的,且n8n通常作为后端服务部署,拥有较高的权限。

3. 漏洞复现环境搭建与验证

纸上得来终觉浅,绝知此事要躬行。要真正理解一个漏洞,最好的办法就是亲手在可控环境中复现它。警告:以下所有操作仅限在您个人完全控制的、隔离的实验室环境(如本地虚拟机或Docker容器)中进行,严禁对任何非授权系统进行测试。

3.1 搭建存在漏洞的n8n环境

为了复现,我们需要一个包含CVE-2025-68613漏洞的n8n版本。根据漏洞时间线,该漏洞影响某个特定版本范围。我们可以使用Docker快速拉起一个易受攻击的n8n实例,这是最干净、最方便的方式。

首先,确保你的实验机安装了Docker和Docker Compose。然后,创建一个docker-compose.yml文件。这里的关键是指定一个包含漏洞的n8n镜像标签。例如,我们可以使用一个稍旧但稳定的版本(注意:此处仅为示例,实际复现应使用漏洞公告中明确指出的受影响版本):

version: '3.8' services: n8n: # 使用一个已知在漏洞影响范围内的版本,例如 n8nio/n8n:0.240.0 image: n8nio/n8n:0.240.0 container_name: vulnerable-n8n restart: unless-stopped ports: - "5678:5678" environment: - N8N_PROTOCOL=http - N8N_HOST=localhost - N8N_PORT=5678 - N8N_EDITOR_BASE_URL=http://localhost:5678/ - WEBHOOK_URL=http://localhost:5678/ - GENERIC_TIMEZONE=Asia/Shanghai - N8N_ENCRYPTION_KEY=super-secret-key-change-me-please - DB_TYPE=sqlite - DB_SQLITE_DATABASE=/home/node/.n8n/database.sqlite volumes: - n8n_data:/home/node/.n8n volumes: n8n_data:

使用命令docker-compose up -d启动容器。稍等片刻,访问http://localhost:5678,你应该能看到n8n的初始化界面,按照指引完成管理员账户创建即可。

实操心得:在实验室环境中,我强烈建议将n8n的数据卷(n8n_data)挂载出来,这样即使容器销毁,工作流数据也能保留,方便反复测试。另外,那个N8N_ENCRYPTION_KEY环境变量在生产环境中必须设置为强随机值,在这里我们仅作演示。

3.2 构造一个易受攻击的示例工作流

漏洞的触发往往需要配合一个设计不当的工作流。我们创建一个最简单的PoC工作流:

  1. 在n8n编辑器中,创建一个新的工作流。
  2. 添加一个“Webhook”节点(用于接收外部HTTP请求,作为触发点)。
  3. 添加一个“Function”节点(或旧版的“Code”节点),连接到Webhook节点之后。
  4. 在Function节点中,编写有问题的代码。例如,我们模拟一个将用户输入直接用于动态表达式求值的场景:
    // 危险示例:未经验证和净化,直接拼接用户输入 const userInput = $input.first().json.payload; // 假设业务逻辑需要将用户输入作为表达式的一部分来求值某个变量 // 这是一种非常危险的模式! const dangerousTemplate = `{{ "User said: " + ${userInput} }}`; // 为了演示,我们尝试用某种方式让这个“模板”被求值。 // 在真实漏洞中,可能是通过设置一个节点参数,该参数支持表达式且值来源于此处。 item.json.vulnerableField = dangerousTemplate; return item;
  5. 再添加一个“Set”节点,尝试去读取或使用vulnerableField,并配置其某个字段的值为表达式模式,且表达式内容来自上一个节点的输出。

这个工作流本身可能无法直接触发漏洞,但它展示了漏洞产生的模式:不可信数据流入了表达式求值上下文。真实的漏洞利用可能需要更精确地控制输入,触发特定节点(如某些社区节点或旧版核心节点)的表达式解析逻辑。

3.3 使用公开PoC进行验证

在安全研究社区,一个漏洞被披露后,通常会有研究者发布概念验证代码。对于CVE-2025-68613,我们可以在GitHub、Exploit-DB等平台搜索相关PoC。假设我们找到了一个利用脚本exploit_n8n_rce.py

在运行任何PoC前,务必阅读代码!确保你理解它的每一步在做什么,避免它对你的实验环境或其他系统造成意外损害。一个负责任的PoC通常只执行无害命令,如idwhoami,并将结果回显。

在实验环境中运行该PoC:

python3 exploit_n8n_rce.py -u http://localhost:5678 -c “id”

如果漏洞存在且环境配置正确,你可能会在PoC的输出中看到服务器执行id命令后的结果,例如uid=1000(node) gid=1000(node) groups=1000(node)。这证实了RCE的可能性。

重要警告:复现过程中,你的n8n实例不应连接任何真实的外部服务(如生产数据库、邮件服务器、内部API),因为执行的任意命令可能会对这些依赖项造成影响。始终在完全隔离的网络中操作。

4. 攻击链路的全手工分析与利用深化

仅仅运行一个自动化PoC脚本是不够的。作为安全从业者,我们需要手工拆解整个攻击链,理解每一个环节,这样才能更好地防御和发现类似问题。

4.1 手动探测与注入点识别

首先,我们需要手动寻找可能的注入点。这需要对n8n的工作流设计有基本了解。我们可以从以下几个方面入手:

  1. 寻找接受用户输入的节点:Webhook, HTTP Request (用于接收), 表单触发,或任何可以配置从外部获取数据的节点。
  2. 寻找执行动态代码或表达式的节点:Function节点、Code节点、以及大量核心节点和社区节点中那些标有“fx”按钮、支持表达式输入的参数框。
  3. 测试输入净化:向这些输入点提交包含表达式语法片段的测试载荷,如{{}}${},观察系统的反应。是直接报错、被过滤、还是原样输出到了某个可能被求值的地方?例如,在Webhook接收的数据中尝试提交:
    { "test": "hello {{ 7*7 }} world" }
    然后,在工作流的后续节点中,仔细观察是否有地方将test字段的值作为表达式求值,并得到了49。如果得到了49,这就是一个明确的表达式注入信号。

4.2 沙箱逃逸载荷的构造与调试

如果确认了注入点,下一步就是尝试突破沙箱。这需要一些JavaScript和Node.js沙箱机制的知识。我们可以尝试一些经典的沙箱逃逸测试载荷:

  • 原型链攻击:尝试通过this.constructor.constructor('return process')()来获取process对象。
  • 查找沙箱暴露的接口:在表达式上下文中,尝试打印thisObject.getOwnPropertyNames(this),看看沙箱环境提供了哪些对象和方法,其中是否有可利用的。
  • 利用n8n内置函数或对象:研究n8n表达式引擎的帮助文档,看是否有内置函数(如$if$min$get等)或对象(如$json,$node)存在非预期的行为或可以用于访问外部原型。

这个过程需要反复试验和调试。我们可以利用Function节点的console.log(如果沙箱允许输出到n8n的执行日志)来调试我们的Payload,或者将尝试的结果赋值给一个变量,并在工作流末尾输出,以观察每一步的执行结果。

4.3 实现稳定RCE的多种路径

成功逃逸沙箱后,实现RCE也有多种路径:

  1. 直接使用child_process:这是最直接的方式,前提是能成功require
    const { exec } = require('child_process'); exec('touch /tmp/pwned', (error, stdout, stderr) => {});
  2. 利用Node.js的VM模块:如果主进程上下文可用,且require('vm')可行,有时可以通过VM模块创建另一个不那么受限的上下文来执行代码。
  3. 写入恶意模块并加载:如果可以写文件(比如通过某些方式将Payload写入/tmp/evil.js),然后通过require('/tmp/evil.js')来加载执行。
  4. 利用已有模块的功能:有时直接执行命令会被监控,可以尝试利用已加载模块的现有功能来实现恶意操作,例如使用fs模块读写敏感文件。

在手工测试时,应从无害命令开始,如echo testwhoamiid,确认通道畅通后,再谨慎测试其他操作。同时,要思考如何将命令执行的结果回传,例如通过DNS外带、HTTP请求将结果发送到攻击者控制的服务器,或者直接写入一个可通过Web访问的静态文件。

5. 影响范围与严重性评估

CVE-2025-68613不是一个可以轻描淡写的小问题。我们来系统评估一下它的影响。

直接影响

  • 权限提升:攻击者从普通工作流调用者或未授权访问者,提升至在n8n服务所在服务器上执行任意命令的权限。n8n通常以node用户或某个特定服务账户运行,这意味着攻击者至少拥有该账户的所有权限。
  • 数据泄露:可以读取n8n的数据库(包含所有工作流配置、存储的凭证、处理过的数据)、服务器上的环境变量、配置文件以及其他应用数据。
  • 横向移动:以n8n服务器为跳板,攻击内网其他服务。如果n8n容器或主机配置不当,甚至可能获取更高权限。
  • 持久化:可以在服务器上安装后门、创建计划任务、写入SSH密钥等,实现持久化访问。
  • 资源滥用:利用服务器进行挖矿、发起DDoS攻击等。

受影响版本:根据漏洞公告,该漏洞影响n8n的某个特定版本范围。所有在此范围内的部署,无论是Docker、npm直接安装、还是云服务自托管版本,均受影响。关键点在于,许多用户可能并不频繁更新其n8n实例,尤其是那些作为内部关键业务自动化组件的部署,它们可能长期运行在一个旧版本上。

部署模式加剧风险

  1. 暴露在公网:为了方便远程配置或触发,很多用户将n8n的管理界面或Webhook监听端口直接暴露在互联网上,仅依赖弱密码或默认密码保护,这大大降低了攻击门槛。
  2. 高权限运行:为了能够调用各种系统命令或访问特定目录,n8n有时会被赋予较高的运行权限,这放大了漏洞成功利用后的破坏力。
  3. 作为集成中枢:n8n的核心价值是集成,因此它通常保存了大量第三方服务的API密钥、数据库凭证、企业内部系统账号等敏感信息。一旦被攻破,损失远不止一台服务器。

6. 立体化防御体系构建

亡羊补牢,为时未晚。针对CVE-2025-68613这类漏洞,我们不能只依赖官方补丁,必须建立一个从外到内、从代码到运维的立体化防御体系。

6.1 紧急缓解与官方补丁应用

这是最直接、最有效的第一步。

  1. 立即升级:前往n8n官方GitHub仓库或公告,确认修复CVE-2025-68613的安全版本号(例如 n8n@0.2xx.x)。立即将你的n8n实例升级到该版本或更高版本。
    • Docker用户:更新docker-compose.yml中的镜像标签,然后执行docker-compose pull && docker-compose up -d
    • npm用户:运行npm update n8n -g或在你项目目录下更新。
  2. 临时缓解:如果因故无法立即升级,考虑以下措施:
    • 网络隔离:立即将n8n实例从公网撤下,确保只能通过VPN或内部网络访问。
    • 强化认证:启用并强制使用多因素认证,检查并删除任何默认或弱密码账户。
    • 审查工作流:临时禁用所有非必要、尤其是包含复杂Function/Code节点或接收外部输入的工作流。

6.2 安全配置加固指南

打补丁是修复漏洞,加固配置是降低被攻击面。

  1. 最小权限原则
    • 绝不以root身份运行n8n。在Docker中,使用user指令指定非root用户。
    • 在宿主机上,严格限制n8n数据卷和配置文件的访问权限。
    • 如果n8n需要调用系统命令,考虑通过一个具有严格权限限制的代理服务或API来进行,而不是直接赋予n8n高权限。
  2. 网络层面隔离
    • 使用反向代理(如Nginx)前置n8n,并配置严格的访问控制列表,仅允许可信IP段访问管理界面。
    • 将Webhook触发端点与管理界面在不同端口或路径上隔离,并对Webhook实施签名验证(如果支持)。
    • 在防火墙规则中,限制n8n容器或主机对外的出站连接,仅允许访问其必需集成的服务。
  3. 凭证与秘密管理
    • 切勿将API密钥、密码等硬编码在工作流中。务必使用n8n的“凭证”功能,它提供加密存储。
    • 定期轮转这些凭证。
    • 考虑使用外部的密钥管理服务来提供动态凭证。
  4. 审计与日志
    • 确保n8n的日志级别足够记录详细的操作和错误信息。
    • 将日志集中收集到SIEM系统,并设置告警规则,例如针对大量失败的登录尝试、异常的工作流执行模式、或Function节点中执行了可疑字符串。

6.3 安全开发与运维最佳实践

从源头和流程上杜绝风险。

  1. 工作流设计安全
    • 输入验证与净化:在任何用户输入被用于表达式、代码或系统命令之前,必须进行严格的验证和净化。对于表达式上下文,要过滤或转义{{}}$等特殊字符。
    • 避免动态代码执行:除非绝对必要,否则避免在Function节点中使用eval()new Function()。优先使用n8n内置节点和表达式函数来完成逻辑。
    • 代码审查:对团队内创建的复杂工作流,特别是包含自定义代码的,应建立同行审查机制。
  2. 依赖与版本管理
    • 订阅n8n的安全公告(如GitHub Security Advisories)。
    • 建立定期更新机制,不要长期运行过旧版本。
    • 同样,关注n8n所依赖的Node.js版本的安全更新。
  3. 纵深防御
    • 在主机层面部署HIDS,监控异常进程创建和文件操作。
    • 考虑在容器环境使用Seccomp、AppArmor等安全配置文件,限制容器的能力。
    • 对n8n实例进行定期的渗透测试和安全评估,模拟攻击者寻找新的弱点。

7. 排查、检测与应急响应

即使做好了防御,也需要有发现入侵和快速响应的能力。

7.1 我是否已经遭受攻击?排查清单

如果你的n8n实例尚未升级,或者怀疑已被入侵,请立即检查以下迹象:

  • 日志中的异常条目
    • 检查n8n应用日志,寻找包含child_processspawnexecevalFunction等关键词的异常执行记录。
    • 查看是否有来自异常IP地址的大量请求,尤其是对特定工作流或Webhook的访问。
    • 关注执行失败但参数可疑的日志。
  • 系统层面的异常
    • 检查服务器上是否出现了非预期的进程、计划任务(crontab)或服务。
    • 检查/tmp/dev/shm等目录是否有可疑的可执行文件或脚本。
    • 使用netstatss命令查看是否有未知的外连连接。
  • n8n内部的异常
    • 审计工作流列表,是否有你不认识的、新创建的、或被修改过的工作流?特别关注那些包含Function/Code节点的工作流。
    • 检查“凭证”列表,是否有凭证被异常使用或更新?
    • 查看执行历史,是否有大量失败或耗时异常的执行记录?

7.2 入侵发生后的应急响应步骤

如果确认被入侵,保持冷静,按步骤处理:

  1. 立即隔离:断开受影响服务器或容器的网络连接,防止攻击者持续控制或横向移动。
  2. 取证备份:在关机前,尽可能完整地备份系统日志、n8n的数据库文件、整个应用目录。这用于后续分析和法律证据。
  3. 评估影响:根据排查结果,初步判断数据泄露范围(哪些凭证、哪些工作流数据被访问)。
  4. 清除后门:基于取证分析,找到攻击者植入的持久化后门(恶意文件、计划任务、账户等)并清除。对于生产系统,更安全的做法是直接废弃当前环境,从干净的备份或镜像重建。
  5. 恢复与加固
    • 使用干净的镜像和已修复的n8n版本重建服务。
    • 从可靠的备份中恢复工作流数据(注意确认备份未被污染)。
    • 强制重置所有存储在n8n内的凭证,因为它们可能已泄露。
    • 实施前面章节提到的所有加固措施。
  6. 复盘与通知:分析攻击根本原因,完善安全流程。如果涉及用户数据泄露,需根据相关法律法规进行通知。

7.3 长期监控策略

建立主动监控,防患于未然:

  • 工作流变更监控:通过n8n的API或日志,监控工作流的创建、更新和删除操作,并设置告警。
  • 异常表达式检测:可以在反向代理层或通过自定义中间件,对传入n8n的请求体进行轻量级扫描,匹配表达式注入的常见模式。
  • 资源使用监控:监控n8n进程的CPU和内存使用情况,突然的异常飙升可能意味着正在执行挖矿等恶意代码。
  • 定期安全扫描:使用漏洞扫描工具定期扫描你的n8n实例和其宿主机环境。

CVE-2025-68613给我们敲响了一记警钟:即使是像n8n这样优秀的、旨在提升效率的工具,如果使用不当或维护疏忽,也会成为安全链上最脆弱的一环。安全不是一个功能,而是一个贯穿设计、开发、部署、运维全生命周期的过程。对于每一位n8n的使用者和运维者来说,理解其运行原理,遵循安全最佳实践,保持组件的更新,是享受其便利性时必须承担的职责。