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」,從讀檔案的權限一路走到能在其他系統之間橫向移動。這種分級寫法本身也是一種訊息,兩件事在技術上可以分開驗證:先確認任意檔案讀取能不能重現,再確認能不能從讀檔案走到執行程式碼。對外部讀者來說,這種寫法比一句籠統的「可以 RCE」更有用——它讓你知道最壞情況分成兩道門檻,中間那一步怎麼跨過去的細節,揭露頁選擇暫時不公開,本文後段會回來談這個取捨。

兩組人各自撞見同一個洞

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」——不需要任何客製化,預設安裝本身就落在受影響範圍內。

這兩句話放在一起讀,其實是在告訴你揭露團隊怎麼劃分風險範圍。「outside its defaults」跟「don't have that requirement」這兩種措辭,把整個受影響族群切成兩塊:一塊需要額外的人為決定才會落入風險(改過設定),一塊完全不需要——只要照著官方文件安裝,就自動落入風險。從防守方的角度看,第二塊的優先順序理所當然要排在第一塊前面,因為它不需要任何前提條件就成立,而第一塊至少還有機會透過「我們沒改過設定」這句話排除自己。

「官方 Docker image 的預設」這件事本身也是 Rails 團隊近年推的方向——用官方映像檔快速起專案、減少環境設定的摩擦,一直是官方文件鼓勵的路徑。這代表這條漏洞命中的,恰好是 Rails 想讓更多人使用的那條路徑。框架的預設值之所以特別危險,不是因為它本身寫得比較差,而是因為選擇預設值等於放棄選擇——大多數團隊裝好 Active Storage 之後不會再回頭檢查圖片處理器用的是哪個函式庫,更不會去追蹤這個函式庫自己的修補紀錄。這是任何框架預設依賴都會遇到的共通處境:只要官方沒特別提醒,使用者就傾向假設「預設值等於安全值」,而 CVE-2026-66066 剛好戳破了這個假設。

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

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

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

圖裡有一條線橫跨三個預設受影響的 release line,那條標「libvips 需另外 ≥ 8.13」的虛線。它畫在版本長條之外,是刻意的:這個門檻跟 Rails gem 版本是兩個獨立的軸,接下來這一段就是在講為什麼要拆成兩軸看。6.x 這條線的表格欄位刻意留白,如實反映揭露頁本身沒有給出對應資訊。比較保守的讀法是:如果你還在維護 6.x 版本的 Rails 服務、又確實動過 Active Storage 的處理器設定,目前公開資料不足以直接對照修補版本,合理的做法是直接聯繫維護團隊確認。這張圖負責回答「範圍在哪裡」,「要怎麼修」留給後面兩節:先把 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 三條線各自的封版更新。研究團隊說得很直接:「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.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,不代表這件事就算處理完。

為什麼利用鏈被藏起來

揭露頁明確寫:「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 設定,理論上可以在這裡停下來,不用往後走。後兩步才是真正的修補動作,兩步都要各自完成才算數。這個順序有它的道理:前兩步用來判斷你是否暴露,後兩步才是實際動手修,兩種判斷不應該混在一起做。

切換分頁看四個步驟 · 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、系統套件管理器,或跟平台維運團隊確認——漏掉這步,前三步全部等於白做。

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

把整篇揭露放在一起看,其實只有三個變數在決定你今天的風險:你在哪條 release line、你的 Active Storage 設定是不是預設值、你的系統 libvips 版本。前兩個變數決定你在不在受影響範圍裡,第三個變數決定你的修補有沒有真的生效。三個變數裡,前兩個查起來只要幾分鐘,第三個往往要跨團隊才能問到答案,理由通常是版本資訊掌握在平台或維運團隊手上,不在應用團隊自己的程式碼倉庫裡。

今天能確認的事:這類「gem 加系統函式庫」的兩層依賴,不會只在 Active Storage 出現一次,任何用原生函式庫做圖片、影音、壓縮處理的 Ruby gem,理論上都可能碰到同樣的修補落差,今天花時間把 libvips 版本查清楚,比只盯著 Gemfile.lock 更值得。利用鏈的細節現在還不公開,Ethiack 自己說得很清楚:留給使用者的是修補視窗,不是技術解說。你今天能確認的只有兩件事:Rails 有沒有升到 7.2.3.2、8.0.5.1 或 8.1.3.1 之後的版本,以及系統的 libvips 有沒有跟著到 8.13 以上。兩者缺一,防線都還是空的——而且缺的那一層,通常不會出現在你平常盯的那份 Gemfile.lock 裡。