跳至主要內容
版本:最新版 (v5.0.x)

原型污染

以下文章由 Eran Hammer 撰寫。 為了後世,此處經許可重製。 它已從原始 HTML 來源重新格式化為 Markdown 來源,但其餘部分保持不變。原始 HTML 可以從上面的許可連結中檢索。

原型污染背後的歷史

根據 Eran Hammer 的文章,此問題是由於網路安全漏洞所造成。 它也是維護開源軟體所需努力以及現有溝通管道限制的完美例證。

但首先,如果我們使用 JavaScript 框架來處理傳入的 JSON 資料,請花點時間閱讀一般性的原型污染,以及此問題的具體技術細節。 這可能是一個嚴重問題,因此我們可能需要先驗證您自己的程式碼。 它著重於特定框架,但是任何使用 JSON.parse() 來處理外部資料的解決方案都可能存在風險。

砰!

Lob 的工程團隊(長期以來對我的工作慷慨支持!)報告了他們在我們的資料驗證模組 joi 中發現的一個嚴重安全漏洞。 他們提供了一些技術細節和建議的解決方案。

資料驗證函式庫的主要目的是確保輸出完全符合定義的規則。 如果不符合,驗證會失敗。 如果通過,我們可以盲目相信您正在使用的資料是安全的。 事實上,大多數開發人員將驗證後的輸入視為從系統完整性的角度來看是完全安全的,這至關重要!

在我們的案例中,Lob 團隊提供了一個範例,其中一些資料能夠逃脫驗證邏輯並未被發現地通過。 這是驗證函式庫可能發生的最糟糕的缺陷。

簡而言之的原型

為了理解這一點,我們需要稍微了解一下 JavaScript 的運作方式。 JavaScript 中的每個物件都可以有一個原型。 它是一組從另一個物件「繼承」的方法和屬性。 我將繼承放在引號中,因為 JavaScript 並不是真正的物件導向語言。 它是一種基於原型的物件導向語言。

很久以前,由於一些不相關的原因,有人決定使用特殊屬性名稱 __proto__ 來存取(和設定)物件的原型,這會是一個好主意。 自此之後,它已被棄用,但仍完全支援。

為了演示

> const a = { b: 5 };
> a.b;
5
> a.__proto__ = { c: 6 };
> a.c;
6
> a;
{ b: 5 }

該物件沒有 c 屬性,但其原型有。 驗證物件時,驗證函式庫會忽略原型,僅驗證物件自身的屬性。 這允許 c 通過原型潛入。

另一個重要的部分是 JSON.parse() 的處理方式 — 該語言提供的將 JSON 格式化的文字轉換為物件的工具 — 如何處理這個神奇的 __proto__ 屬性名稱。

> const text = '{"b": 5, "__proto__": { "c": 6 }}';
> const a = JSON.parse(text);
> a;
{b: 5, __proto__: { c: 6 }}

請注意 a 如何具有 __proto__ 屬性。 這不是原型參照。 它是一個簡單的物件屬性鍵,就像 b 一樣。 正如我們從第一個範例中看到的那樣,我們實際上無法通過賦值來建立這個鍵,因為這會調用原型魔法並設定實際的原型。 然而,JSON.parse() 設定了一個具有該有害名稱的簡單屬性。

就其本身而言,JSON.parse() 建立的物件是完全安全的。 它沒有自己的原型。 它有一個看似無害的屬性,恰好與內建的 JavaScript 魔術名稱重疊。

然而,其他方法就沒那麼幸運了

> const x = Object.assign({}, a);
> x;
{ b: 5}
> x.c;
6;

如果我們採用先前由 JSON.parse() 建立的 a 物件,並將其傳遞給有用的 Object.assign() 方法(用於將 a 的所有頂級屬性執行淺複製到提供的空 {} 物件中),則神奇的 __proto__ 屬性會「洩漏」並變成 x 的實際原型。

驚喜!

如果您取得一些外部文字輸入並使用 JSON.parse() 剖析它,然後對該物件執行一些簡單的操作(例如,淺複製並新增 id),並將其傳遞給我們的驗證函式庫,它會通過 __proto__ 未被偵測地潛入。

天啊,joi!

當然,第一個問題是,為什麼驗證模組 joi 會忽略原型並讓潛在有害的資料通過? 我們也問了自己同樣的問題,我們的立即想法是「這是一個疏忽」。 一個錯誤 - 一個非常大的錯誤。 joi 模組不應該允許這種情況發生。 但是...

雖然 joi 主要用於驗證網頁輸入資料,但它也有相當多的用戶群體使用它來驗證內部物件,其中一些物件具有原型。 joi 忽略原型的事實是一個有用的「功能」。 它允許驗證物件自身的屬性,同時忽略可能非常複雜的原型結構(具有許多方法和文字屬性)。

在 joi 層級的任何解決方案都意味著會破壞一些目前正在運作的程式碼。

正確的事情

此時,我們正在處理一個破壞性的嚴重安全漏洞。 就在史詩級安全失敗的較高層級。 我們所知道的是,我們極受歡迎的資料驗證函式庫無法阻止有害資料,而且這種資料很容易溜過去。 您所需要做的只是將 __proto__ 和一些垃圾新增到 JSON 輸入中,並將其發送到使用我們工具建置的應用程式中。

(戲劇性地停頓一下)

我們知道我們必須修復 joi 以防止這種情況發生,但考慮到這個問題的嚴重性,我們必須以一種方式來修復,以便在不會過於引人注意的情況下推出修復程式 — 至少在大多數系統收到更新之前,不要太容易被利用。

偷偷推出修復程式並不是最難的事情。 如果您將其與程式碼的其他無目的的重構結合起來,並加入一些不相關的錯誤修復,也許還加入一個很酷的新功能,您就可以發布新版本,而不會引起對正在修復的真正問題的注意。

問題是,正確的修復程式會破壞有效的用例。 您看,joi 無法知道您是希望它忽略您設定的原型,還是阻止攻擊者設定的原型。 修復漏洞的解決方案將會破壞程式碼,而破壞程式碼往往會引起很多注意。

另一方面,如果我們發布一個正確的(語義版本化)修復程式,將其標記為重大變更,並新增一個新的 API 以明確告知 joi 您希望它如何處理原型,我們將與全世界分享如何利用這個漏洞,同時也使系統升級更加耗時(重大變更永遠不會由建置工具自動套用)。

繞道而行

雖然手邊的問題是關於傳入的請求負載,但我們不得不暫停並檢查它是否也會影響通過查詢字串、Cookie 和標頭傳入的資料。 基本上,任何從文字序列化為物件的內容。

我們很快確認了節點預設查詢字串剖析器以及其標頭剖析器都沒問題。 我發現 base64 編碼的 JSON Cookie 以及自訂查詢字串剖析器的使用可能存在一個問題。 我們還編寫了一些測試,以確認最受歡迎的第三方查詢字串剖析器 — qs — 沒有漏洞(它沒有!)。

一項進展

在整個分類過程中,我們只是假設帶有有毒原型的有問題輸入是從 hapi(連接 hapi.js 生態系統的 Web 框架)進入 joi 的。 Lob 團隊的進一步調查發現,問題更加細緻。

hapi 使用 JSON.parse() 來處理傳入的資料。 它首先將結果物件設定為傳入請求的 payload 屬性,然後將同一個物件傳遞給 joi 進行驗證,然後再傳遞給應用程式商業邏輯進行處理。 由於 JSON.parse() 實際上不會洩漏 __proto__ 屬性,因此它會帶著無效的鍵到達 joi 並導致驗證失敗。

但是,hapi 提供了兩個擴充點,可以在驗證之前檢查(和處理)負載資料。 這些都已適當地記錄下來,並且大多數開發人員都了解。 這些擴充點的存在是為了讓您在驗證之前與原始輸入互動,以便用於合法(且通常與安全性相關)的原因。

如果在這兩個擴充點之一中,開發人員在負載上使用了 Object.assign() 或類似的方法,__proto__ 屬性就會洩漏並變成實際的原型。

鬆了一口氣

我們現在正在處理截然不同的糟糕程度。 在驗證之前操作負載物件並不常見,這意味著這不再是世界末日的情況。 它仍然可能具有災難性,但暴露範圍從每個 joi 使用者降至一些非常特定的實作。

我們不再期待秘密的 joi 發布。 joi 中的問題仍然存在,但我們現在可以在未來幾週內使用新的 API 和重大發布來正確解決它。

我們也知道,由於框架知道哪些資料來自外部,哪些是內部產生的,因此我們可以在框架層級輕鬆緩解此漏洞。 框架實際上是唯一可以保護開發人員避免犯下如此意想不到的錯誤的部分。

好消息、壞消息、沒有消息?

好消息是這不是我們的錯。 這不是 hapi 或 joi 中的錯誤。 這只有透過 hapi 或 joi 所獨有的複雜動作組合才有可能發生。 這可以發生在每個其他 JavaScript 框架中。 如果 hapi 壞了,那麼世界就壞了。

太棒了 — 我們解決了責怪遊戲。

壞消息是,當沒有人可以責怪(除了 JavaScript 本身)時,很難將其修復。

當發現安全問題時,人們第一個會問的問題就是是否會發布 CVE。CVE(通用漏洞披露)是一個已知安全問題的資料庫。它是網路安全的重要組成部分。發布 CVE 的好處是它會立即觸發警報,並通知相關人員,而且通常會中斷自動建置,直到問題解決。

但我們應該把它歸咎於什麼呢?

可能什麼都不歸咎。我們仍在爭論是否應該在某些版本的 hapi 上標記警告。「我們」指的是節點安全流程。由於我們現在有一個新版本的 hapi 預設可以解決此問題,因此可以將其視為一個修復。但由於此修復並非針對 hapi 本身的問題,因此宣稱舊版本有害並不完全合理。

僅僅為了提醒人們注意並升級而發布關於先前版本 hapi 的建議,是對建議流程的濫用。我個人很樂意為了提高安全性而濫用它,但這不是我能決定的。截至撰寫本文時,這仍在爭論中。

解決方案的商業考量

緩解這個問題並不難。讓它具有可擴展性和安全性則需要更多的工作。由於我們知道有害數據可以從哪裡進入系統,並且知道我們在哪裡使用了有問題的 JSON.parse(),我們可以將其替換為安全的實作方式。

但有一個問題。驗證數據的成本可能很高,而且我們現在計劃驗證每個傳入的 JSON 文字。內建的 JSON.parse() 實作速度很快。真的非常快。我們不太可能建立一個更安全且速度同樣快的替代方案。尤其不可能在一夜之間完成,而且不引入新的錯誤。

很明顯,我們將用一些額外的邏輯來包裝現有的 JSON.parse() 方法。我們只需要確保它不會增加太多額外的開銷。這不僅僅是效能上的考量,也是安全上的考量。如果我們讓系統很容易因為發送特定數據而變慢,我們就很容易以非常低的成本執行阻斷服務攻擊

我想出了一個非常簡單的解決方案:首先使用現有工具解析文字。如果沒有失敗,則掃描原始的原始文字中是否有冒犯性的字串「proto」。只有找到它,才對物件執行實際掃描。我們無法阻止所有對「proto」的引用,有時它是完全有效的值(例如,當在這裡撰寫相關內容並將此文字傳送到 Medium 發布時)。

這使得「正常路徑」實際上與以前一樣快。它只增加了一個函式呼叫、快速文字掃描(再次,非常快速的內建實作),以及一個條件式返回。該解決方案對預期通過的大多數數據幾乎沒有影響。

下一個問題。prototype 屬性不一定位於傳入物件的頂層。它可以深層嵌套在內部。這表示我們不能只檢查它是否存在於頂層。我們需要遞迴地迭代整個物件。

雖然遞迴函式是很常用的工具,但在撰寫注重安全性的程式碼時,它們可能會造成災難性的後果。您知道,遞迴函式會增加執行階段呼叫堆疊的大小。迴圈次數越多,呼叫堆疊就越長。在某個時間點 — 砰 — 您會達到最大長度,並且程式會終止。

如果您無法保證傳入數據的形狀,則遞迴迭代會成為一個公開的威脅。攻擊者只需要製作一個足夠深的物件,就可以使您的伺服器崩潰。

我使用了平面迴圈實作,它不僅更有效率(減少函式呼叫、減少傳遞暫時參數),而且更安全。我並不是為了炫耀而指出這一點,而是為了強調基本工程實務如何產生(或避免)安全陷阱。

測試

我將程式碼發送給兩個人。首先發送給 Nathan LaFreniere,以複檢解決方案的安全性屬性,然後發送給 Matteo Collina,以檢視效能。他們都是各自領域的佼佼者,而且常常是我的首選求助對象。

效能基準測試證實,「正常路徑」實際上沒有受到影響。有趣的發現是,移除冒犯性值比拋出例外更快。這引發了關於新模組的預設行為應該是什麼的問題 — 我將其稱為 bourne — 錯誤或清理。

再一次,擔憂的是應用程式暴露在阻斷服務攻擊的風險中。如果發送包含 __proto__ 的請求會使速度降低 500%,這可能是一個容易利用的攻擊途徑。但在進行更多測試後,我們確認發送任何無效的 JSON 文字都會產生非常相似的成本。

換句話說,如果您解析 JSON,無論是什麼使其無效,無效的值都會讓您付出更高的代價。同樣重要的是要記住,雖然基準測試顯示掃描可疑物件的成本百分比很高,但 CPU 時間的實際成本仍然在毫秒級。值得注意和測量,但實際上並無害。

hapi 從此過著幸福快樂的日子

有很多事情值得感謝。

Lob 團隊最初的披露非常完美。它以私下方式報告給正確的人,並提供正確的資訊。他們跟進了其他發現,並給予我們時間和空間以正確的方式解決問題。多年來,Lob 也是我在 hapi 上的工作的主要贊助商,而這種財務支持對於讓一切順利進行至關重要。稍後會詳細介紹。

分流過程壓力很大,但由合適的人員組成。有像 Nicolas Morel、Nathan 和 Matteo 這樣的人隨時待命並渴望提供幫助至關重要。如果沒有壓力,這件事就不容易處理,但有了壓力,如果沒有適當的團隊合作,就很容易犯錯。

我們很幸運地遇到了實際的漏洞。一開始看起來像是一場災難性的問題,結果卻是一個細緻但直接的問題,可以解決。

我們也很幸運能夠完全存取原始碼來緩解問題 — 不需要向一些未知的框架維護者發送電子郵件,並希望得到快速的回覆。hapi 對其所有依賴項的完全控制再次證明了它的實用性和安全性。未使用 hapi也許您應該考慮一下

幸福快樂的結局之後

在這裡,我必須藉此事件重申永續且安全的開源的成本和需求。

我光是處理這個問題就花了超過 20 個小時。這是一週工作的一半時間。它發生在一個月結束時,我已經花了 30 多個小時發布 hapi 的新主要版本(大部分工作是在 12 月完成的)。這使得我本月個人財務損失超過 5000 美元(我不得不減少付費客戶工作,以便騰出時間)。

如果您依賴我維護的程式碼,這正是您想要(而且老實說 — 期望)的支援、品質和承諾水準。你們大多數人都認為這是理所當然的 — 不僅僅是我的工作,還有數百名其他敬業的開源維護人員的工作。

由於這項工作很重要,我決定嘗試使其不僅在財務上可持續,而且還要使其成長和擴展。還有很多需要改進的地方。這正是促使我實施 3 月即將推出的新的商業授權計畫的原因。您可以在這裡閱讀更多相關資訊。