vatt'ghern jaskier's ballads

多數 Rails RCE 需要你先做錯一件事——套件版本沒鎖、設定被改鬆。CVE-2026-66066 有一項條件不用你做錯:只要照 Rails 官方 Docker image 的預設設定跑 Active Storage,圖片處理器這一項就自動符合條件之一——但符不符合另外兩項,還得你自己查。

預設設定的代價:Active Storage 的 RCE

CVE-2026-66066 是 Ethiack 研究團隊在 2026 年 7 月 29 日揭露的一條漏洞鏈,起點是 Active Storage 對圖片做 variant 處理時的任意檔案讀取,終點是遠端程式碼執行與橫向移動。單一漏洞鏈在資安揭露裡很常見,讓 CVE-2026-66066 特別的是它命中的範圍:即使你什麼都沒改、只用官方預設值跑 Active Storage,處理器這一項就已經自動命中受影響條件之一——完整判定還要另外兩個條件,本文後面會列清楚。

發現這條鏈的不只一組人。揭露頁寫 Ethiack 研究團隊的 André Baptista、Bruno Mendes 與 Rafael Castilho 三人一起找到它,幾天後 GMO Flatt Security 的 RyotaK 也獨立回報了同一個漏洞。揭露頁把影響拆成兩級:先是「Arbitrary file read as the user running the worker or process」,再往上升級成「Remote code execution and lateral movement to other systems」,從讀檔案的權限一路走到能在其他系統之間橫向移動。這種分級寫法本身也是一種訊息:兩件事在技術上可以分開驗證,讓你知道最壞情況分成兩道門檻。中間那一步怎麼跨過去的細節,揭露頁選擇暫時不公開,本文後段會回來談這個取捨。

兩組人各自撞見同一個洞

Ethiack 的三人是一組,GMO Flatt Security 的 RyotaK 是另一組,兩邊沒有互相參考,卻在幾天之內先後得出同一個結論。放在資安研究的脈絡裡看,這通常意味著這條攻擊面沒有想像中冷門:如果只有一組人、用了某種罕見的分析手法才找到,代表這個洞比較難被獨立重現;但兩組人在短短幾天內分別撞上,代表只要對 Active Storage 的 variant 處理流程有基本的好奇心,這個方向並不難走到。「critical」這個標籤在這裡值得認真對待,即使揭露頁沒有附上 CVSS 分數;兩組人各自獨立撞見同一個洞這件事替這個判斷加了分,是我的看法,揭露頁沒這樣論證。

沒有 CVSS 分數不代表可以往後排。多數團隊的修補優先順序表是照 CVSS 分數切門檻的,遇到「揭露方自己標 critical、但沒有官方評分」這種狀況,流程上容易卡住——不知道該套哪一格門檻,乾脆先放著等分數出來。比較穩妥的做法,是直接把前面那兩個已知的影響等級當優先順序依據,不用等第三方評分系統補一個數字才動手。

為什麼「預設」才是重點

多數 Rails 應用裝 Active Storage 之後,圖片 variant 要用哪個處理器,你通常沒有主動選過。揭露頁寫得很直接:vips 就是「the processor that ships by default in the official Rails Docker images」,照官方路徑部署,這個依賴已經是安裝自帶的一部分。揭露頁特別把「預設設定」跟「非預設設定」分開講,因為同一個漏洞在不同 release line 上要滿足的前提條件不一樣。6.0.0 到 6.1.7.10 這條線,原文寫的是「may be affected only if Active Storage has been set up outside its defaults」——換句話說,標準安裝不算,得先動手改過設定才會踩到。但到了 7.0.0 到 7.2.3.1、8.0.0 到 8.1.3 這幾條線,原文換了說法:「don't have that requirement」,意思是不需要任何客製化,預設安裝本身就落在受影響範圍內。

不過「處理器是 vips」只是受影響的其中一個條件,不是全部。揭露頁把完整條件列得很清楚:「An application is affected only if it meets all of the following: Uses libvips for Active Storage image processing; Allows image uploads from untrusted users via features or direct uploads; Runs a libvips build linked against certain third-party libraries」。用 vips 只滿足第一項,還要應用本身接受不受信任來源的圖片上傳,而且系統的 libvips 建置又連結了特定第三方函式庫,三項同時成立才算數。「預設」保證的只有第一項,這也是本文反覆強調它的原因,但符合這一項不等於受影響。

這兩句話放在一起讀,其實是在告訴你揭露團隊怎麼劃分風險範圍:一塊需要額外的人為決定才會落入風險(改過設定),一塊完全不需要動手改設定——照官方 Docker image 裝好,處理器這一項就已經成立。從防守方角度看,第二塊的優先順序理所當然排在第一塊前面。

框架的預設值之所以特別危險,不是因為它本身寫得比較差,而是因為選擇預設值等於放棄選擇,大多數團隊裝好 Active Storage 之後不會再回頭檢查圖片處理器用的是哪個函式庫,更不會去追蹤這個函式庫自己的修補紀錄。這是任何框架預設依賴都會遇到的共通處境:只要官方沒特別提醒,使用者就傾向假設「預設值等於安全值」。CVE-2026-66066 戳破的只是「處理器用 vips」這一項假設——揭露頁講得很清楚,光是這一項並不足以讓你受影響。

把四條 release line 攤開看,這個對比會更清楚。下面這張圖把「哪條線需要客製設定才中」跟「哪條線預設就中」擺在一起看,再疊上一條不隨 gem 版本走的 libvips 門檻線。長條寬度是分類呈現,不代表版本號之間有什麼可比的數值距離。

橫向捲動看完整版本列 · 4 條 release line

資料來源:Ethiack 揭露頁列出的版本範圍與修補版本;長條寬度為分類呈現,不代表版本號之間的數值距離。

圖裡有一條線橫跨三個預設受影響的 release line,那條標「libvips 需另外 ≥ 8.13」的虛線。它畫在版本長條之外,是刻意的:這個門檻跟 Rails gem 版本是兩個獨立的軸,接下來這一段就是在講為什麼要拆成兩軸看。6.x 這條線的修補版本,揭露頁列的同樣是 7.2.3.2——如果你還在維護 6.x 版本的 Rails 服務、又確實動過 Active Storage 的處理器設定,修補路徑不是留在 6.x 原地修,而是直接升級到 7.2.3.2。這張圖負責回答「範圍在哪裡」,「要怎麼修」留給後面兩節:先把 gem 修補版本跟 libvips 門檻擺在同一張表裡對照,再把對照結果拆成四個可以照做的步驟。

兩層修法:gem 之外還有 libvips

揭露頁給出的三個修補版本是 7.2.3.2、8.0.5.1、8.1.3.1,分別對應 7.2.x、8.0.x、8.1.x 三條線各自的封版更新(6.x 若曾偏離預設設定,適用的同樣是 7.2.3.2 這個目標)。研究團隊說得很直接:「Upgrading is the fix that actually matters here: move to Rails 7.2.3.2, 8.0.5.1, or 8.1.3.1 (or later)」,聽起來像一句標準資安公告的收尾,但接下來那句話才是這篇揭露真正值得停下來讀的地方:「The fix depends on libvips itself being version 8.13 or later, earlier libvips builds can't disable the unsafe operations involved at all」。也就是說,Rails gem 端的修補程式碼本身沒有能力在舊版 libvips 上把危險的操作關掉——不是「舊版也能用、只是比較不安全」,而是「舊版 libvips 上這個修補根本無法生效」。揭露頁把結論講得很白:「If you're on libvips < 8.13, upgrading active storage alone won't be enough, you'll need to upgrade libvips too」。

兩層修法對照:Rails gem 的修補版本,與獨立於 gem 版本之外的系統 libvips 需求。資料來源:Ethiack 揭露頁。
release line 受影響版本 預設設定 Rails 修補版本 libvips 最低版本
6.0.x 至 6.1.x6.0.0 至 6.1.7.10僅非預設才受影響7.2.3.2未提及
7.2.x7.0.0 至 7.2.3.1預設即受影響7.2.3.2≥ 8.13
8.0.x8.0.0 至 8.0.5預設即受影響8.0.5.1≥ 8.13
8.1.x8.1.0 至 8.1.3預設即受影響8.1.3.1≥ 8.13

這種兩層修法在維運上比一行 bundle update 麻煩得多。Rails gem 版本寫在 Gemfile.lock 裡,CI 跑 bundle update、看 diff,就知道有沒有升級到位。libvips 不是 gem,是系統層的原生函式庫,可能包在容器裡、系統套件裡,或雲端平台管理的 runtime 裡,沒有一個路徑會因為你跑了 bundle update 就自動跟著動。一個團隊完全可能把 Rails gem 升到 7.2.3.2、CI 顯示綠燈、PR 也合併了,卻因為 base image 裡的 libvips 還停在更早的版本,實際上仍然暴露在原本的漏洞之下,而且沒有任何工具會主動提醒這件事,因為套件管理系統只看 gem,不看系統套件。揭露頁為什麼把 libvips 版本單獨拉出來講一句,原文沒解釋;比較合理的猜測是——完整的修補鏈本來就包含 gem 與系統函式庫兩層,兩層都要做到才算真正修好。

接下來這段是維運層面的推想,揭露頁沒有談:這個落差在三種常見的部署形態裡各自有不同的長相。如果 Rails 應用直接用官方 Docker image 當 base image,libvips 版本就跟著 base image 的標籤走,base image 沒更新,應用重建再多次也沒用。如果是自己維護的虛擬機器或實體機器,libvips 版本掌握在維運團隊手上,而維運團隊往往不會把「這台機器上的 libvips 是幾版」放進日常檢查清單。如果是用雲端平台代管的 Rails runtime,libvips 版本完全交給平台決定,應用團隊連查都不一定查得到——這種情況下,升級 gem 之後能做的,可能只剩下去問平台什麼時候會把底層映像檔更新到含 libvips 8.13 以上的版本。

如果你手上不只一個 Rails 應用

單一應用的檢查清單容易做,真正麻煩的是同時維護十幾個、幾十個 Rails 服務的團隊——每個服務可能停在不同的 release line,用不同的 base image,由不同的人在不同時間點建立。這種情況下,「確認 Rails 版本落在哪條線」這一步,實際上要跑過整個服務清單才算數。更麻煩的是 libvips 這一層:同一個組織裡,不同服務常走不同的部署路徑(前面提過的三種形態),確認方法也不一樣,得逐一處理。把這件事做完,關鍵是把版本檢查寫進 CI 或映像檔掃描流程裡,讓它變成每次建置都會自動跑一次的步驟。實務上,libvips 版本通常可以透過系統套件管理器的查詢指令,或是在容器內直接檢查函式庫版本資訊取得,這件事的動作本身簡單,重點在於把它變成每個服務都會跑一次的固定流程。

這種盤點也常卡在「這是誰的責任」這個問題上。Rails gem 版本歸應用團隊管;libvips 版本依我的觀察更常歸屬平台團隊、SRE,或共用的容器映像檔維護者,這些人不見得知道用到了 libvips,也不見得看得到這篇揭露。升級 gem 只補完兩層修法裡的一層,另一層要等系統的 libvips 版本跟上才算到位。

為什麼利用鏈被藏起來

揭露頁明確寫:「We are withholding technical details, PoC, and the RCE details at the moment to give users a reasonable window to patch」,研究團隊選擇不公開任意檔案讀取怎麼被串成遠端程式碼執行的技術細節,理由是給使用者一個合理的修補視窗。這代表這篇報導能講的,就只有已經公開的部分:受影響範圍、修補版本、libvips 的前提條件。至於任意檔案讀取具體讀到什麼、又怎麼從讀檔案走到執行程式碼,揭露頁沒有寫。任何試圖幫你補完這段的說法都是猜測,不是這篇揭露本身的內容。對讀者比較實際的態度,是把它當成一個已知會發生、但攻擊細節未知的風險,先按公開的版本資訊行動,而不是花時間去推測攻擊者可能怎麼組合這兩個環節。下面這四個步驟只圍繞在版本比對打轉,因為版本比對是目前能立即驗證、不需要等待更多細節的動作;另外揭露頁也給了兩個不用等升級就能先擋下來的緩解措施,本文稍後會列出來。

這種取捨在資安揭露裡不算新鮮:公開技術細節能幫助防守方驗證,但同一份細節也是攻擊者的路線圖。揭露頁只給了一句理由:窗口優先於透明。剩下能做的,就是把揭露頁給出的版本資訊,轉成一份今天就能跑一遍的檢查清單。

防守方今天能做的四件事

這四步刻意分成兩半。前兩步不涉及任何修補動作,純粹是在回答「我需要擔心嗎」,如果你在 6.x 這條線又從沒動過 Active Storage 設定,理論上可以在這裡停下來,不用往後走。後兩步才是真正的修補動作,兩步都要各自完成才算數。這個順序有它的道理:前兩步用來判斷你是否暴露,後兩步才是實際動手修,兩種判斷不應該混在一起做。這四步只涵蓋 Rails 版本與設定這條軸線,另外兩個受影響條件——是否接受不受信任來源的圖片上傳、系統 libvips 建置連結了哪些函式庫——不在這四步裡,得回頭對照前面列出的完整條件。

切換分頁看四個步驟 · 4 個步驟

確認你在哪條 release line 上:對照 Rails 版本落在 6.0.0 到 6.1.7.10、7.0.0 到 7.2.3.1、8.0.0 到 8.0.5,還是 8.1.0 到 8.1.3。6.x 這條線只有在 Active Storage 被設成非預設狀態時才需要擔心,其他三條線光是預設安裝就已經符合「處理器」這一項條件。
確認 Active Storage 是不是用預設設定:如果你在 6.x 這條線,而且從沒特別動過 Active Storage 的處理器設定,理論上不在暴露面裡;如果動過設定,或你在 7.x、8.x 任一條受影響線上,處理器這一項幾乎不用查,但還要另外兩個條件都成立,才算真的受影響。
把 Rails gem 升到對應修補版:7.2.x 線升到 7.2.3.2,8.0.x 線升到 8.0.5.1,8.1.x 線升到 8.1.3.1,或之後的版本。這一步在 Gemfile.lock 上看得到、CI 跑得出來,是整個修補流程裡最容易被完整執行的一段。
單獨確認系統的 libvips 版本:進容器或伺服器裡跑版本檢查,確認 libvips 版本達到 8.13 以上。這一步不會出現在 bundle update 的輸出裡,也不會反映在 Gemfile.lock 上,得另外去查 base image、系統套件管理器,或跟平台維運團隊確認——漏掉這步,前三步全部等於白做。

這四步都假設你打算走升級這條路,揭露頁另外給了兩個先擋一手的做法,但前提要看清楚。原文寫:「There's also a stopgap if you can't upgrade immediately: on libvips 8.13+, setting the VIPS_BLOCK_UNTRUSTED environment variable will block the exploitation of this vulnerability」。關鍵在句首那個 on libvips 8.13+:這個環境變數本身就要求 libvips 已經到 8.13 以上,所以它救不了還卡在舊版 libvips 的人,擋的是 Rails gem 一時推不上去的空窗。另一條路針對不同前提:「If you're instead pinned to ruby-vips 2.2.1 or newer, calling Vips.block_untrusted(true) from an initializer also prevents this」。兩者都不能取代升級。

第四步不在任何人平常會去看的地方,版本管理系統、部署面板、監控儀表板,沒有一個預設會把系統函式庫版本擺在顯眼位置。它不像 gem 版本那樣有 Gemfile.lock 可以核對,也不會因為排程掃描就自動浮現,得靠人記得主動去查一次,而且通常要另外去問負責映像檔或系統套件的團隊才查得到。

把整篇揭露放在一起看,release line、Active Storage 設定、系統 libvips 版本這三個變數,前兩個查起來只要幾分鐘,第三個往往要跨團隊才問得到答案,理由通常是版本資訊掌握在平台或維運團隊手上,不在應用團隊自己的程式碼倉庫裡。

今天能確認的事:這類「gem 加系統函式庫」的兩層依賴,不會只在 Active Storage 出現一次,任何用原生函式庫處理圖片、影音、壓縮的 Ruby gem,理論上都可能碰到同樣落差。利用鏈的細節現在還不公開,Ethiack 說得很清楚:留給使用者的是修補視窗,不是技術解說。你今天能確認的至少有三件事:Rails 有沒有升到 7.2.3.2、8.0.5.1 或 8.1.3.1 之後的版本、系統的 libvips 有沒有跟著到 8.13 以上,以及——若 libvips 已經在 8.13 以上、只是 gem 一時推不上去——VIPS_BLOCK_UNTRUSTED 這個環境變數有沒有先設起來。版本比對缺一,防線就還是空的;環境變數不是升級的替代品,前提也是 libvips 夠新,而且不會出現在你平常盯的那份 Gemfile.lock 裡。