Rails CVE-2026-66066 が公開された。Active Storageのvariant(画像バリアント)処理でlibvipsの危険な操作が有効なまま残っていたため、未認証の攻撃者が細工した画像をアップロードするだけでサーバー上の任意ファイルを読み出せる。CVSS 4.0で 9.5(Critical) の Active Storage 脆弱性で、影響を受けるgemはactivestorage、修正版は 7.2.3.2 / 8.0.5.1 / 8.1.3.1 の3本だ。2026年に公表されたRails 脆弱性のなかでもスコアが高く、しかも修正版に上げると環境によってはアプリが起動しなくなるという副作用を抱えている。
この記事はサプライチェーン全体の防御を扱うサプライチェーンセキュリティ2026|攻撃手法・防御ツール・実践チェックリストの個別事例として、CVE-2026-66066の影響判定と対処に絞って整理する。
created_at(ISO 8601)をJSTへ換算した実測値。activestorageの3本は7分36秒の間に並んで出ている。まず自分の環境が該当するかを測る。以下は3つの層をそれぞれ確認するコマンドで、Railsアプリのルートで実行する。
# ① activestorage のバージョン(7.2.3.2 / 8.0.5.1 / 8.1.3.1 未満なら影響範囲内)
bundle list | grep -E "activestorage|ruby-vips|image_processing"
grep -A1 -E "^ (activestorage|ruby-vips) " Gemfile.lock
# ② variant_processor が :vips か :mini_magick か(:vips が今回の該当側)
bin/rails runner 'puts ActiveStorage.variant_processor'
# ③ libvips が 8.13 以上か = 修正の前提を満たすか
# Active Storage が起動時に見るのと同じ判定をそのまま実行する
bin/rails runner 'require "vips"; puts "libvips=#{Vips::LIBRARY_VERSION} ruby_vips=#{Vips::VERSION} blockable=#{Vips.respond_to?(:block_untrusted)}"'
③のblockableがfalseなら、修正版のRailsに上げた時点でアプリが起動しなくなる。先にlibvipsとruby-vipsを上げる必要がある。この順序が今回いちばん事故になりやすい部分だ。
この記事のポイント(30秒でわかる)
・何が起きたか:Active Storageのvariant処理でlibvipsの「unfuzzed」操作が無効化されておらず、細工した画像経由で任意ファイル読み出し。CVE-2026-66066 / CVSS 9.5 / CWE-1188(安全でない既定値での初期化)
・影響条件は3層:activestorageのバージョン × variant_processorが:vipsか × libvips/ruby-vipsのバージョン。①だけ見ても判定できない
・上げる前に測る:修正の実体はVips.block_untrusted(true)の呼び出しで、この関数はlibvips 8.13以上でしか定義されない。Ubuntu 22.04(8.12.1)・Debian 11(8.10.5)の標準パッケージのままだと起動時にRuntimeError
・7.1以下に修正版は無い:GHSAの範囲は< 7.2.3.2と下限が無く7.1/7.0/6.xも含むが、リリースされたのは3ブランチ分だけ。回避策か7.2.3.2以降への移行のみ
・パッチだけでは終わらない:secret_key_base・master key・DB・各サービスの資格情報の入れ替えが必要。読めた状態は巻き戻らない
Rails CVE-2026-66066とは——Active Storageのvariant処理で何が読み出されるのか
GHSA-xr9x-r78c-5hrmが公表した内容を、確定している事実だけで並べる。
Active Storageは添付画像のサムネイルなどを生成するときに、variant_processorで指定された画像処理エンジンを使う。選択肢は:vips(libvipsをruby-vips経由で使う)と:mini_magick(ImageMagick)、それに:disabledだ。今回の脆弱性は:vipsの経路に存在する。
libvipsは、自身が抱える画像フォーマットのローダー/セーバーのうち一部を「unfuzzed」あるいは「untrusted」として印を付けている。ファジングテストが十分に回っていない、あるいはサードパーティのフォーマットライブラリへ処理を委譲するもので、信頼できるコンテンツにしか使ってはいけないという区分だ。libvips自身は8.13以降でこれらを一括で無効化する仕組み(vips_block_untrusted_set)を用意している。
Active Storageはこの無効化を呼んでいなかった。結果として、Webの一般的な画像形式(JPEG/PNG/WebP等)だけを想定していたはずの経路に、unfuzzedなローダーが到達可能な状態で残った。GHSAはこの状態をCWE-1188「Initialization of a Resource with an Insecure Default(安全でない既定値によるリソースの初期化)」に分類している。脆弱性の本体はメモリ破壊バグではなく、危険な機能を既定で閉じていなかったという構成の問題だという整理だ。
Rails公式の告知は影響を次のように書いている。「既定の構成において、画像variantを表示するRailsアプリケーションは、未認証の攻撃者にサーバー上の任意ファイル——プロセスの環境変数を含む——の読み出しを許す可能性がある」。読み出せる対象として具体的に挙げられているのはsecret_key_base、Railsのmaster keyとcredentials、データベースのパスワード、クラウドストレージの資格情報、APIトークンだ。
報告者はEthiackの0xacb・s3np41k1r1t0・castilhoの3名と、GMO Flatt SecurityのRyotaK氏。研究側は「kindarails2shell」という名前でこの件を公開しているが、攻撃チェーンの詳細は悪用を遅らせるために伏せられている。当記事も同じ方針で、PoCや攻撃再現の手順は扱わない。
なお、影響の書き方には注意が必要だ。GHSAとRails公式はいずれも「任意ファイル読み出しと、リモートコード実行の可能性」という並列表記をとる。直接の帰結は任意ファイル読み出しで、そこからRCEへ至る道は「Rails環境では複数の方法で可能とされる」という位置づけだ。読み出し単体でもsecret_key_baseが取得できればセッション・署名・暗号化の前提が崩れるため、RCEの成立可否を待たずに深刻と評価してよい。
CVSSの内訳
CVSS 4.0のベクタはCVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:Hで、スコアは9.5。ネットワーク経由(AV:N)、権限不要(PR:N)、ユーザー操作不要(UI:N)で、機密性・完全性・可用性がいずれも高影響、さらに後続システムへの影響(SC/SI/SA)も高いという評価になっている。AT:P(Attack Requirements: Present)が付いているのは、後述するvariant_processorとlibvipsの構成条件があるためだ。
2026-07-30 07:16 JST時点で、このCVEはNVD(National Vulnerability Database)にまだ登録されていない。NVDのAPIにcveId=CVE-2026-66066で問い合わせるとtotalResults: 0が返る。NVDの登録を待ってから対応を始めると出遅れるため、判断はGHSAとRails公式の告知を一次ソースにするのが正しい。
なぜファイル読み出しに至るのか——unfuzzed操作という区分
仕組みを、公開されている範囲の事実で組み立てる。
libvipsは多数の画像フォーマットに対応しているが、その対応は一枚岩ではない。JPEG・PNG・WebPのような主要形式は専用の実装が入っている一方、それ以外の形式は外部ライブラリへの委譲や、利用実績の少ないローダーで処理される。libvipsはこの後者の一部に「untrusted」の印を付け、vips -lでクラス階層とともに確認できるようにしている。
ここに、Webアプリの構造上の前提が重なる。Active Storageで画像をアップロードできるアプリは、ファイルの中身を攻撃者が完全に制御できる。そしてvariant生成は、アップロードされたファイルの実際の形式に応じてローダーを選ぶ。つまり攻撃者がファイルの中身を通じて、どのローダーを起動するかを選べる状態になる。unfuzzedなローダーが閉じられていなければ、選択肢にそれらが含まれる。
細工したファイルをアップロード"] --> B["Active Storage
添付として保存"] B --> C{"variant_processor"} C -->|":mini_magick"| D["ImageMagick で処理
今回の経路は成立しない"] C -->|":vips"| E["ruby-vips → libvips"] E --> F{"unfuzzed 操作は
ブロックされているか"} F -->|"修正版
block_untrusted(true)"| G["unfuzzed ローダー/セーバーを拒否
Vips::Error"] F -->|"未修正
呼ばれていない"| H["ファイル内容に応じた
ローダーが選択される"] H --> I["任意ファイル読み出し
プロセスの環境変数を含む"] I --> J["secret_key_base・master key
DB・ストレージの資格情報が露出"] J --> K["RCE へ至る経路が
複数あるとされる"]
修正版がやっていることは、この分岐を起動時に閉じることだ。activestorage 8.1.3 と 8.1.3.1 のgemを展開して差分を取ると、変更は4ファイルと新規1ファイルだけの外科的な修正だとわかる。新規追加されたlib/active_storage/vips.rbが中核で、ruby-vipsを読み込んだ直後に次を実行する。
・Vips.respond_to?(:block_untrusted)で、無効化する機能が使えるかを確認する
・使えなければRuntimeErrorを投げて起動を止める
・使えるならVips.block_untrusted(true)を呼び、unfuzzed操作を一括で無効化する
重要なのは、これがバージョン文字列の比較ではなく機能が存在するかの検査だという点だ。ruby-vips側のソースを読むと理由がわかる。block_untrustedメソッドはif at_least_libvips?(8, 13)というブロックの内側で定義されていて、リンクされている実際のlibvipsが8.13未満ならメソッドそのものが生成されない。さらにこのラッパー自体がruby-vips 2.2.1で入ったものなので、gemが古い場合も同様に存在しない。どちらが原因でも「機能が無い」という同じ結果になるため、Rails側はrespond_to?で一度に判定している。
Active Storageはimage_processing/vipsの読み込み順にも手を入れている。追加されたコメントによれば、image_processing 2.0は自分でVips.block_untrusted(true)を呼ぶため先に読み込ませる必要があり、後から読み込むとアプリのイニシャライザが再有効化した設定を上書きしてしまう。無効化を確実に効かせるための順序制御だ。
Rails CVE-2026-66066の影響範囲マトリクス——修正版があるのは3ブランチだけ
GHSAが宣言している影響範囲は次の3つだ。
・activestorage < 7.2.3.2
・activestorage >= 8.0, < 8.0.5.1
・activestorage >= 8.1, < 8.1.3.1
1つ目に下限が無いことが実務上いちばん重要だ。< 7.2.3.2という表記は7.2系だけを指しておらず、7.1.x・7.0.x・6.x以前もすべて含む。しかしRailsがリリースしたのは3ブランチ分の修正版だけで、7.1以下には修正版が存在しない。
| activestorage | GHSAの影響範囲 | 修正版 | load_defaultsの既定 |
|---|---|---|---|
| 8.1.0 – 8.1.3 | 該当 | 8.1.3.1 | :vips |
| 8.0.0 – 8.0.5 | 該当 | 8.0.5.1 | :vips |
| 7.2.0 – 7.2.3.1 | 該当(< 7.2.3.2) |
7.2.3.2 | :vips |
| 7.1.x | 該当(< 7.2.3.2) |
無し | :vips |
| 7.0.x | 該当(< 7.2.3.2) |
無し | :vips |
| 6.1以前 | 該当(< 7.2.3.2) |
無し | :mini_magick |
load_defaultsの列は、railtiesのlib/rails/application/configuration.rbを読んで確認した実装どおりの値だ。active_storage.variant_processor = :vipsはwhen "7.0"のブロックに置かれている。そして各バージョンのブロックはload_defaultsを再帰的に呼んで下位の設定を積み上げる作りなので(8.1→8.0→7.2→7.1→7.0)、load_defaults 7.0以降を指定しているアプリはすべて既定で:vipsになる。
ライブラリ自体の既定値はactive_storage.rbで:mini_magickのままだ。したがってload_defaultsが6.1以前のアプリは、明示的に:vipsを設定していない限り今回の読み出し経路には乗らない。6.x系が範囲に含まれていても実際の露出は限定的、という差はここから来る。
整理すると、影響判定は次の組み合わせで決まる。
variant_processor |
activestorageが影響範囲内 | 今回の読み出し経路 | 修正版へ上げるときの起動要件 |
|---|---|---|---|
:vips |
はい | 成立する | 適用される |
:vips |
いいえ(修正済み) | 塞がれている | 適用される |
:mini_magick(ruby-vipsあり) |
はい | 成立しない | 適用される |
:mini_magick(ruby-vipsなし) |
はい | 成立しない | 適用されない |
:disabled |
はい | 成立しない | ruby-vips次第 |
3行目が見落とされやすい。CHANGELOGは「:mini_magickのvariant processorを使っているアプリケーションは添付の処理方法に変化は無いが、ruby-vipsがインストールされている限りローダーとセーバーはプロセス全体で無効化され、下記のバージョン要件も適用される」と書いている。ImageMagickを使っているから無関係、とは言えない。要件を避けたい場合はGemfileからruby-vipsを外すのが公式に案内された手だ。
なお、ディストリビューションのカーネルやパッケージのバージョンが影響有無を左右する構図は今回に限らない。Copy Fail(CVE-2026-31431)解説:Linuxカーネル脆弱性とEC2/ECS/EKSへの影響でも、動いている基盤のバージョンを実測しないと判定できない点が同じだった。今回はそれがlibvipsという画像ライブラリで起きている。
自システムの確認手順——3層をコマンドで判定する
冒頭のコマンドを、層ごとに意味を添えて展開する。
① activestorageのバージョン
railsのバージョンとactivestorageのバージョンは通常一致するが、確認するのはactivestorage側にしておく。Gemfile.lockが正解を持っている。
# ロックファイルから実際に使われている版を読む
grep -E "^ (activestorage|actionpack|railties) \(" Gemfile.lock
grep -E "^ (ruby-vips|image_processing) \(" Gemfile.lock
# bundler経由での確認
bundle list | grep -E "activestorage|ruby-vips|image_processing"
# コンテナの中で動いているものを見る場合
docker compose exec web sh -c 'grep -E "^ activestorage \(" Gemfile.lock'
bundle listはGemfile.lockの解決結果を表示するため、gem listより信頼できる。gem listはシステムに入っている全バージョンを並べるので、アプリが実際に読んでいる版とずれることがある。
② variant_processorの実効値
設定ファイルをgrepするだけでは足りない。config/application.rbや環境別ファイルに明示指定が無ければload_defaultsの値が効くため、実際に解決された値を見る必要がある。
# 実効値を出す(これが :vips なら今回の該当側)
bin/rails runner 'puts ActiveStorage.variant_processor'
# 明示指定と load_defaults の指定を突き合わせる
grep -rn "variant_processor" config/
grep -rn "load_defaults" config/application.rb
ActiveStorage.variant_processorはイニシャライザ完了後に確定するため、bin/rails runnerで読むのが確実だ。:vipsが返れば該当、:mini_magickか:disabledなら今回の読み出し経路そのものは成立しない。
③ libvipsとruby-vips
ここが今回の固有部分だ。3つの見方を用意する。
# CLIから見たlibvipsのバージョン
vips --version
# アプリがリンクしている実際のlibvipsとgemの版、そして無効化が可能か
bin/rails runner 'require "vips"; puts "libvips=#{Vips::LIBRARY_VERSION}"; puts "ruby-vips=#{Vips::VERSION}"; puts "blockable=#{Vips.respond_to?(:block_untrusted)}"'
# libvipsのクラス階層を一覧し、untrusted印の付いた操作を探す
vips -l | head -40
2つ目のblockableが、Active Storageが起動時に見るのとまったく同じ判定だ。falseなら修正版で起動できない。vips --versionとアプリから見えるVips::LIBRARY_VERSIONは、複数のlibvipsが同居している環境や、コンテナとホストで違う場合にずれることがあるので、アプリのプロセスから見た値を優先する。
ディストリビューションの標準パッケージだけで見ると、境界は次のようになる。
| ベースイメージ / OS | 標準libvips | 8.13要件 |
|---|---|---|
| Ubuntu 22.04 LTS (jammy) | 8.12.1 | 未満 |
| Debian 11 (bullseye) | 8.10.5 | 未満 |
| Debian 12 (bookworm) | 8.14.1 | 満たす |
| Ubuntu 24.04 LTS (noble) | 8.15.1 | 満たす |
| Debian 13 (trixie) | 8.16.1 | 満たす |
Ubuntu 22.04 LTSが8.12.1で止まっているのが厄介だ。8.13にわずかに届かないだけだが、判定はat_least_libvips?(8, 13)なので通らない。まだ広く使われているLTSであり、ruby:3.x-bullseye系のイメージも同様に該当する。ruby-vips側は2.2.1が2024-02-21公開なので、2.2.0以前を使い続けているアプリも要件未満になる。
④ 信頼できないアップロードを受け付けているか
GHSAが挙げている条件のうち、コマンドで機械的に判定できないのがこれだ。「untrusted user uploads enabled」——つまり攻撃者がファイルの中身を制御できる経路があるかどうか。ログインが必要なアプリでも、アカウントを誰でも作れるなら攻撃者は「信頼できないユーザー」に含まれる。判断の材料になるのは次の点だ。
・添付を受け付けるモデルとコントローラの一覧(has_one_attached / has_many_attachedの宣言箇所)
・そのエンドポイントに到達できるのが誰か(未認証・登録すれば誰でも・招待制・社内のみ)
・アップロードされた画像に対してvariantを生成しているか(variant / preview / representationの呼び出し)
・variant生成がリクエスト中か、バックグラウンドジョブか
# 添付を持つモデルとvariant生成箇所を洗い出す
grep -rn "has_one_attached\|has_many_attached" app/models/
grep -rn "\.variant(\|\.representation(\|\.preview(" app/ | head -20
# direct uploadのエンドポイントが有効かどうか
grep -rn "direct_upload\|rails_direct_uploads" app/ config/ | head -10
has_*_attachedがあってvariantを生成していて、そのエンドポイントに未認証または誰でも登録できる状態で到達できるなら、条件は満たされていると考えるのが妥当だ。逆に、添付は管理画面からのみ・アップロードするのは運用担当者だけ、という構成なら露出は限定される。ただしこの場合も「限定される」であって「無い」ではないため、修正版への更新自体は先送りしない前提で計画したい。
読み出せる対象が.env相当の資格情報である点は、BISSA Scanner解析:AI支援の大規模脆弱性スキャンと.envクレデンシャル窃取の仕組みで扱った、露出した資格情報が機械的に収集されていく流れと地続きだ。読み出しの成立と資格情報の悪用開始の間隔は短いと考えて動くほうが安全側になる。
対策——上げる前に測り、上げた後に秘密情報を回す
手順1:先に③を測る
前節のblockableがfalseのまま修正版へ上げると、アプリは起動時にRuntimeErrorで落ちる。デプロイパイプラインの中で気づくとロールバック対応になるため、先に測る。
bin/rails runner 'require "vips"; puts Vips.respond_to?(:block_untrusted)'
要件を満たさないまま上げてしまった場合、起動時に投げられるメッセージは修正版のソースに文言が固定されている。次の内容だ。
libvips’s unfuzzed operations are not safe to use with untrusted content, and Active Storage cannot disable them. Disabling them requires libvips 8.13 or later and ruby-vips 2.2.1 or later. Please upgrade libvips and ruby-vips, or remove the ruby-vips gem from your Gemfile.
デプロイ後にアプリが起動しなくなったとき、この文字列でログを検索すれば原因を即座に特定できる。CIやデプロイのログ収集側にあらかじめ検知ルールを入れておくのも有効だ。
# デプロイログ/コンテナログから起動失敗の原因を特定する
grep -r "unfuzzed operations are not safe" log/ 2>/dev/null
docker compose logs web 2>&1 | grep -i "unfuzzed\|block_untrusted"
falseだった場合、libvipsとruby-vipsを先に上げる。Dockerを使っているならベースイメージをDebian 12(bookworm)以降やUbuntu 24.04へ移すのが素直だ。
# ruby-vips gem を要件以上へ
bundle update ruby-vips
grep -E "^ ruby-vips \(" Gemfile.lock
# libvips 側はイメージ/OSのパッケージを更新(例:apt系)
apt-get update && apt-get install -y --only-upgrade libvips42 libvips-tools
vips --version
手順2:修正版へ上げる
# 該当ブランチの修正版へ
bundle update rails activestorage
grep -E "^ activestorage \(" Gemfile.lock
# 起動できることを確認する(ここでRuntimeErrorが出たら手順1へ戻る)
bin/rails runner 'puts "boot ok: #{ActiveStorage.variant_processor}"'
上げる先は使っているブランチに対応する 7.2.3.2 / 8.0.5.1 / 8.1.3.1 のいずれか。7.1以下には修正版が無いため、次の回避策か、7.2.3.2以降へのマイナーバージョン移行を選ぶことになる。
手順3:上げられない場合の回避策
libvipsが8.13以上あれば、Railsを上げずにunfuzzed操作を無効化できる。GHSAとRails公式が案内している2つの方法だ。
# 方法A:環境変数。libvipsが初期化時に読む(libvips 8.13以上)
VIPS_BLOCK_UNTRUSTED=1 bin/rails server
# 方法B:イニシャライザから呼ぶ(ruby-vips 2.2.1以上)
# config/initializers/vips_block_untrusted.rb
require "vips"
Vips.block_untrusted(true) if Vips.respond_to?(:block_untrusted)
方法Aは環境変数を設定するだけなので、コード変更が難しい状況でも入れやすい。ただしどちらもlibvips 8.13以上が前提であることは変わらない。8.13未満の環境では、回避策も修正版も同じ壁に当たる。その場合はlibvipsを上げるか、variant_processorを:mini_magickへ切り替えて:vips経路を使わなくするか、:disabledにしてvariant生成を止めるかの判断になる。
修正版のリリースが複数の保守ブランチへ同時に出るという形は、パッケージエコシステムでよく見る対応の形だ。Composer 脆弱性 CVE-2026-40261 PerforceドライバRCE、2.9.6/2.2.27で修正でも2系統のブランチへ同時に修正版が出ていた。どのブランチに乗っているかで上げ先が変わるため、Gemfile.lockの実値から逆算するのが確実になる。
手順3.5:無効化が実際に効いているかを確かめる
上げた/回避策を入れた後、無効化が本当に効いているかはアプリのプロセスから確認できる。イニシャライザの読み込み順の問題(後からimage_processing/vipsが読まれて設定が戻る、など)で意図せず外れることがあるため、宣言ではなく実測で見る。
# untrusted扱いの操作が実際にブロックされているかを確認する
bin/rails runner 'require "vips"; puts "blockable=#{Vips.respond_to?(:block_untrusted)} libvips=#{Vips::LIBRARY_VERSION}"'
# unfuzzed扱いの形式が拒否されるようになったかを、無害なファイルで確かめる
# (BMPは修正版でvariant生成が Vips::Error になる想定の形式)
bin/rails runner 'require "vips"; begin; Vips::Image.new_from_buffer(File.binread("sample.bmp"), ""); puts "loaded (blocked ではない)"; rescue Vips::Error => e; puts "blocked: #{e.message.lines.first}"; end'
2つ目のコマンドは、手元にある無害なBMPファイルで挙動だけを確かめるものだ。blocked:が出れば無効化が効いている。loadedが返る場合は、variant_processorの設定やイニシャライザの順序を見直す。どちらのコマンドも攻撃を再現するものではなく、防御が有効かどうかの確認に留まる。
どの操作がuntrusted扱いなのかを網羅的に見たい場合は、libvipsのCLIが持つクラス階層の一覧が使える。
# libvipsのクラス階層を出力する。untrusted印の付いた操作はここから辿れる
vips -l | head -40
ruby-vipsのソースには「vips -lをコマンドラインで使うとクラス階層と、どの操作がuntrustedとしてマークされているかを確認できる」というコメントが付いている。出力の表記形式は環境のlibvipsバージョンによって変わるため、特定の文字列でgrepするのではなく階層を目で追うのが確実だ。
手順4:秘密情報を入れ替える
ここが省略されやすい。この脆弱性で読み出せるのはアプリのプロセスが読めるファイルであり、パッチは「すでに読まれていたかもしれない」という過去を巻き戻さない。GHSAは対策手順として明示的に秘密情報のローテーションを挙げている。
・secret_key_base
・master key(config/master.key)とcredentials.yml.encの中身
・データベースのパスワード
・Active Storageのストレージサービスの資格情報(S3等のアクセスキー)
・アプリが保持している外部APIのトークン
secret_key_baseはセッションcookieの署名・暗号化、signed_id、message verifierの鍵導出に使われる。漏れていた場合の影響はセッション偽造まで及ぶため、入れ替えと同時にセッションの無効化も検討対象になる。入れ替え作業そのものは、リスク評価の結果として「露出していた可能性が低い」と判断できる場合を除き、実施する前提で計画したほうがよい。
修正版で壊れるもの——BMP/ICO/PSDのvariantと起動時エラー
修正版のCHANGELOGは、この変更を明確に「破壊的変更(breaking change)」と呼んでいる。セキュリティ修正だからそのまま上げて終わり、とはいかない部分がある。
失敗するようになるのは次のケースだ。
・BMP・ICO・PSDの添付に対するvariant変換 → Vips::Error
・SVG・JPEG XL・JPEG 2000・Netpbmなどの解析でwidthとheightを記録しなくなる(BMP・ICO・PSDも同様)
・unfuzzedな出力形式の要求(典型的にはFITS、JXL、ImageMagickへ委譲されるもの)→ Vips::Error
・libvips 8.13未満 / ruby-vips 2.2.1未満での起動 → RuntimeError
一方で、添付(attach)・保存(store)・ダウンロード(download)は変わらない。壊れるのはvariant生成と解析のメタデータ部分に限られる。
CHANGELOGが挙げている運用上の注意が実務的だ。「これは、バックグラウンドジョブではなくリクエスト中に画像を変換しているアプリケーションで問題になりやすい。失敗がジョブの失敗ではなくエラーレスポンスとして表面化するからだ」。同期的にvariantを生成している構成では、上げた直後にユーザー向けの500として現れる可能性がある。
対処としてCHANGELOGが提示しているのは、該当する content type をvariant対象から外す方法だ。
# config/initializers/active_storage_variable_types.rb
Rails.application.config.active_storage.variable_content_types -=
%w[ image/bmp image/vnd.microsoft.icon image/vnd.adobe.photoshop ]
これを入れると、Active Storageはそれらの添付を「variant化できないもの」として扱い、variantを生成しようとしなくなる。既存のBMP/ICO/PSD添付を抱えているアプリは、上げる前に該当する添付が存在するかを数えておくと影響を見積もれる。
# 該当content_typeの添付がどれだけあるか
bin/rails runner 'puts ActiveStorage::Blob.where(content_type: ["image/bmp","image/vnd.microsoft.icon","image/vnd.adobe.photoshop"]).count'
特定のローダーやセーバーがどうしても必要な場合は、イニシャライザで個別に再有効化する道もCHANGELOGに書かれている。ただし再有効化はその形式について今回の危険性を戻す判断になるため、信頼できる入力に限られている経路かどうかを確認したうえで選ぶことになる。
過去のActive Storage variant処理の脆弱性との関係
Active Storageのvariant処理には前例がある。2025年8月に公開されたCVE-2025-24293(GHSA-r4mg-4433-c7g3)は、変換メソッドの既定の許可リストに安全性チェックを回避できるメソッドが含まれており、変換メソッドや引数が信頼できない入力である場合にコマンドインジェクションにつながりうる、というものだった。修正版は 8.0.2.1 / 7.2.2.2 / 7.1.5.2 で、このときは7.1系にも修正版が出ている。
2つは別のCVEで、原因も違う。前者は許可リストの中身、今回は危険な操作を既定で閉じていなかったこと。ただし「variant処理は攻撃者が制御できる入力を、多機能な画像処理ライブラリへ渡す場所である」という構図は共通している。今回7.1系に修正版が出なかったのは、そのブランチが既にセキュリティ修正の対象期間を外れたためと考えられ、前回の経験から「7.1にも来るはず」と待つと対応が止まる点に注意したい。
参照ソース
・GHSA-xr9x-r78c-5hrm — Possible arbitrary file read and remote code execution in Active Storage variant processing(Rails公式アドバイザリ。CVE番号・CVSS・影響範囲・修正版・回避策の一次ソース)
・[CVE-2026-66066] Possible arbitrary file read and remote code execution in Active Storage variant processing — Ruby on Rails Discussions(Rails公式のセキュリティ告知)
・activestorage のバージョン一覧 — RubyGems(修正版の公開時刻を確認したソース。本文の日時はAPIのcreated_atをJST換算)
・kindarails2shell — GMO Flatt Security Blog(報告者による研究解説。攻撃チェーンの詳細は非公開)
・vips — Debian Sources(各Debianリリースの標準libvipsバージョン)
・ruby-vips — RubyGems(block_untrustedが入った2.2.1の公開日)