從 HTTP2Shell 的角度看兼顧安全性的程式語言選擇
大家好,我是無能。
前陣子,Brotli 解碼器被發現存在漏洞,似乎被稱為 Node2Shell 或 HTTP2Shell 等名稱。
也就是 CVE-2026-111497,在解壓縮異常的 Brotli 壓縮資料時會發生越界寫入,可能導致程序崩潰、記憶體資訊洩漏,甚至在某些條件下執行任意程式碼。
也就是說,由於問題出在解碼器端的實作,HTTP 用戶端也需要更新,而伺服器端若有啟用 Brotli,我想也需要進行更新。不過若論影響程度,我認為 HTTP 用戶端受到的影響更大。
這次想談的與其說是漏洞本身,不如說是當這類問題發生時,採用的語言與函式庫組成,會讓修補程式送達使用者的難易程度產生相當大的差異。
不是更新作業系統的函式庫就一定能解決
例如,用戶端若使用作業系統的共享函式庫 libbrotli,基本上就能透過各作業系統的套件管理員安裝修正版來處理。當然,前提是發行版已提供修補,而且執行中的程序可能需要重新啟動。
不過,這指的是應用程式以動態連結方式使用作業系統共享函式庫的情況。
即使同樣使用 Brotli,如果在建置時將函式庫靜態連結進執行檔,更新作業系統端的 libbrotli 也不會改變二進位檔內的內容。必須等待開發者發布修正版,或自行更新相依套件並重新建置。
換句話說,重點不只是「用哪種語言撰寫」,而是「有問題的程式碼包含在哪裡,又會透過誰的更新而被替換」。
Rust 的二進位檔發布很方便,但另一方面
以 Rust 來說,一般以 Rust 撰寫的 crate 多半會被整合進執行檔,方便以單一二進位檔發布,這一點很便利。其實我自己也相當喜歡這種便利性。
另一方面,當相依的程式碼被發現有漏洞時,可能會出現只更新作業系統套件也無法修復的情況。若要修補包含在二進位檔中的相依程式碼,原則上就必須重新建置並重新發布應用程式。
這次的 Brotli 也是如此,從 Rust 呼叫 C 實作的 brotli2 這類包裝函式庫,與使用 Rust 實作的 brotli 這類函式庫,情況並不相同。即使使用 C 實作,更新方式也會因為是動態連結共享函式庫,還是連 C 程式碼都一併靜態連結而有所不同。並不是說只要是 Rust 製作的軟體,就一定受到這次 C 實作漏洞的影響。
說到底,Rust 的記憶體安全性,與相依程式碼是否容易更新,是兩個不同的問題。
我認為 Rust 在函式庫開發與重視效能的部分非常有幫助,但若用於廣泛發布的應用程式,就必須連發布後的安全性更新一起考量。能夠產生快速且自給自足的二進位檔,所帶來的另一面,就是將修補送達使用者的責任,也更偏向由應用程式端承擔。
Node.js 可以透過更新執行環境處理,比較輕鬆
Node.js 也受到這次漏洞的影響。由於 Brotli 的實作整合在 Node.js 本身之中,只更新作業系統的 libbrotli 並無法解決,必須更新至修正版的 Node.js。
以這次來說,Node.js 的安全性更新版本已發布 22.23.4、24.22.1、26.12.1。
不過,若是一般的 Node.js 應用程式,只要更新 Node.js 這個執行環境並重新啟動應用程式,就能套用其中包含的 Brotli 修補。當然,如果 npm 套件端包含另一個有漏洞的實作,或將 Node.js 本身打包進執行檔,則另當別論。
這方面,我覺得某種程度上比 Rust 的單一二進位檔發布更容易維運。因為有些情況下,可以透過更新共用的執行環境來處理,不需要讓每位使用者重新建置大量應用程式。
話雖如此,並不是只要使用 Node.js 就會觸發這次的漏洞。只有在存在解壓縮異常 Brotli 資料的處理路徑時,才會成為問題。印象中,HTTP 伺服器特地對收到的請求進行 Brotli 解壓縮的實作並不算多;但反過來說,以用戶端身分存取外部服務的 fetch(),會自動解壓縮 Brotli 壓縮的回應,因此也不是只檢查伺服器監聽端就夠了。
以前我比較偏向反對 Node.js,但看到近來標準 API 的充實程度,對它的印象已經改變不少。像是 fetch()、Web Streams、測試執行器等,以前得依賴外部套件的部分,現在光靠 Node.js 本身就已經能涵蓋相當多。
只要能減少相依套件,需要管理更新的對象也會隨之減少。與以前相比,現在我對 Node.js 作為應用程式開發選項的評價已經相當正面。
我還是覺得 Go 的設計很出色
在這方面,我認為 Go 是一門在設計時就相當重視應用程式開發的語言。
以 net/http 為首的標準函式庫很實用,建立 HTTP 用戶端或伺服器時,往往不必引入大量外部相依套件,只靠標準套件就能完成大部分實作。標準 HTTP 用戶端基本上只會自動解壓縮 gzip,也沒有預設使用這次 Google Brotli 的 C 解碼器。
當然,Go 通常也會將相依程式碼整合進二進位檔,因此只要那些程式碼有漏洞,就需要重新建置與重新發布。並不是使用 Go 就只需更新作業系統函式庫;如果透過 cgo 使用外部函式庫,也一樣會遇到動態連結或靜態連結的問題。
即使如此,能夠只靠標準套件完成的範圍很廣,而且容易將必要的相依套件維持在較少的數量,我認為這不僅對可維護性,對安全性也有相當大的幫助。
選擇程式語言時,該考量的不只有執行效能
這並不是在說 Rust 不好,或 Node.js、Go 就安全。說到底,這次的漏洞存在於 Brotli 的 C 實作中,不能光憑語言名稱判斷是否受到影響。
不過,在選擇語言時,除了執行效能、記憶體用量與開發效率之外,我認為也應該評估相依函式庫的更新,最終由誰、透過什麼方式送達並套用到使用者端。
如果是共享函式庫,就有可能透過作業系統套件更新一次修好。另一方面,靜態連結的二進位檔雖然能連同相依程式碼一起自給自足,但修補時就需要逐一更新各個應用程式。像 Node.js 這樣採用執行環境的模式,雖然有能統一更新執行環境本體的優點,但若執行環境本身出現漏洞,影響範圍也會很廣。
歸根究柢,這是要把什麼整合到哪裡、如何發布,以及要負責維護到什麼程度的設計問題。
就我個人而言,雖然我想積極將 Rust 用於開發函式庫或需要效能的元件,但看到這次的 HTTP2Shell,也讓我再次感受到:若是一般應用程式,選擇 Go 或 Node.js 的理由其實也相當充分。