Miqto
自分で立てて測った記録

依存の脆弱性を到達可能性で切る

CVSS 9.8 の勧告が出ても、それが自分に刺さるとは限りません。CVSSは「条件が揃ったときの深刻さ」であって、その条件が自分の環境で揃っているかは含まれていないからです。
Astro の AVIF経由のRCE を題材に、Astro 7.2.2 の最小構成をゼロから立てて画像エンドポイントの応答を実測しました。設定を1か所ずつ変えると、404 → 403 → 200 と変わります。
そのまま走る再現スクリプトを置きました。測っていない範囲も書いてあります。

9.8勧告のCVSSAVIF経由のRCE・v3
404全ページ静的のとき/_image が存在しない
403オンデマンド1本のとき外部URLは拒否される
200image.domains 追加後許可先の画像を取得して変換

01勧告には成立条件が書いてある

GHSA-26w7-cxv4-gfx2(2026-09-08公開・CVSS v3 9.8)の本文は短く、要点は2文です。

GitHub Advisory より(原文)
A vulnerability in libheif, used by the default Sharp image service in Astro, can lead to remote code execution when a malicious AVIF image is optimized.

Projects are affected when an attacker can cause Astro to process an untrusted AVIF image.

2文目が成立条件です。「攻撃者が untrusted な AVIF を Astro に処理させられること」。裏を返せば、攻撃者が画像を渡せないなら条件は成立しません。

CVSS は「条件が揃ったときの深刻さ」の数値です。「その条件が自分の環境で揃っているか」は、点数のどこにも入っていません。だから点数を見ただけでは、直す順番すら決められません。

点数の順は、刺さる順ではない
同じ astro 7.2.2 には、GHSA-376h-93r7-7g6f(CVSS 6.3・認可バイパス)もあります。設定した base を取り除くときにパスの区切りを見ていなかったため、base: "/app" に対して /appX/admin が内部では /admin に解決される一方、middleware は /appX/admin を見る、というものです。

条件は①ルート以外の base を設定している ②middleware が context.url.pathname で認可しているの2つ(ANDです)。

両方を満たす構成なら、9.8 より先に手を打つべきものになります。画像を1枚も扱っていなくても効くからです。点数順に読んでいたら、これを後回しにします。

02条件が成立するか、自分で立てて測る

Astro で外部から画像を渡せる口は 画像エンドポイント /_image です。ここが開いているか、開いていても何を受け付けるかを測ります。

稼働中のサイトを叩いて終わりにはしません。「404 が返った」が「存在しない」なのか「今日は落ちている」なのか区別できないからです。Astro 7.2.2 の最小構成を手元に立て、設定だけを変えて比較しました。

測った3つの構成(違いは1か所ずつ)
A
node アダプタあり
全ページ prerender
B
A + prerender = false のルートを1本だけ追加
C
B + image.domains に外部ホストを1つ許可
B の「オンデマンドのルート1本」は、実務では問い合わせフォームの送信先がこれに当たります。静的サイトでもフォームがあれば1本持つのが普通です。
実測値(astro 7.2.2 / @astrojs/node 11.1.2 / sharp 0.35.4・2026-09-18)
構成/_image(引数なし)外部URL
許可していないホスト
外部URL
許可したホスト
A 全ページ prerender404404404
B オンデマンド1本400403403
C B + image.domains400403200 image/webp
  1. A:完全に静的なサイトには /_image が存在しないnode アダプタを積んでいても、全ページがプリレンダーされるならエンドポイントごと生えません。画像はビルド時に変換済みで、実行時の変換口がありません。
  2. B:オンデマンドのルートを1本持つと /_image が現れるただし外部URLは 403 Forbidden。プロジェクト内の画像しか受け付けません。攻撃者が任意のAVIFを渡す経路がない=9.8 の条件は成立しません。
  3. C:許可リストに足したホストの画像は、取りに行って処理するimage.domains に1行足しただけで 200 image/webp が返りました。設定1行で到達可能になるということです。
つまり危ないのは「許可リストに何を載せたか」
image.domainsremotePatterns に載せた先が攻撃者の影響下にあるなら、そこが untrusted 入力の入口になります。

具体的には 利用者がアップロードした画像を置くバケットユーザー投稿を配信するCDN外部の画像プロキシ。「自社のドメインだから安全」とは限りません。そのホストに誰が書き込めるかで決まります。

※ この実験で測ったのは到達可能性だけです。悪意ある AVIF の作成・投入は行っていません。sharp も修正版の 0.35.4 を明示的に入れており、脆弱な 0.35.3 は動かしていません。「入口が開いているか」と「入れたら何が起きるか」は別の作業で、後者は攻撃コードの実行にあたるため、自分の環境であっても踏み込んでいません。また外部サイトは一切叩かず、「外部ホスト」の役もローカルに立てた小さなサーバーで代用しています。

03この測り方がカバーしないもの

結論を持ち帰る前に、測っていない範囲を先に書いておきます。ここを飛ばすと、この手法はただの言い訳になります。

測ったのは1経路だけ
GET /_image?href= のみです。remotePatterns による許可(試したのは image.domains だけ)、getImage() をサーバー側コードから呼ぶ経路astro dev(開発サーバー)の挙動は測っていません。
アプリ自身の入口は別
アプリが画像アップロード機能を持っているなら、そこが直接の入口です。/_image がいくら閉じていても関係ありません。
A は実務ではあまり成立しない
A の 404 は「プロジェクト全体にオンデマンドのルートが1本も無い」ことに依存します。問い合わせフォームが1つでもあれば B 以降です。A のままでいられることは、あまりありません。

04これは「Astroの脆弱性」ではなかった

再現環境を作る過程で、勧告の構造が見えました。npm レジストリで astro の依存指定を並べると一目瞭然です。

astro が宣言している sharp のバージョン範囲(いずれも optionalDependencies
astrosharp の指定
7.2.2 / 7.2.3 / 7.2.4 / 7.2.7^0.34.0 || ^0.35.0
7.2.8 / 7.3.3^0.35.4

9.8 に対する「修正」の実体は、sharp の下限を 0.35.4 に上げたことです。欠陥は sharp が使う libheif にあります。

※ 当初ここに「Astro 側のコードは変わっていない」と書いていましたが、言い過ぎでした。7.2.7 と 7.2.8 の tarball を展開して全ファイルを比較すると、差分は7ファイル。5つはバージョン文字列の刻印、1つが package.json の sharp 指定、残る1つが dist/core/dev/lockfile.jsfind-processfind-proc という依存の差し替え(開発サーバーのロック処理で、画像とは無関係)です。変わっていないのは画像処理まわりであって、コード全体ではありません。

ここから、直感に反する状態が生まれます。OSV に問い合わせた結果です。

astro 7.2.2
2件該当(9.8 と 6.3)
astro 7.2.8
0件
sharp 0.35.3
1件該当
sharp 0.35.4
0件
誰でも確認できます:curl -s -X POST -d '{"package":{"name":"astro","ecosystem":"npm"},"version":"7.2.2"}' https://api.osv.dev/v1/query
astro 7.2.2 + sharp 0.35.4 は、欠陥が無いのに警告され続ける
astro の範囲指定 ^0.34.0 || ^0.35.00.35.4 も許します。実際、この構成で新規にインストールすると sharp は 0.35.4 が入りました。

この状態はlibheif の欠陥を持っていません。それでもスキャナは astro 7.2.2 を見て 9.8 を報告します。スキャナが見ているのは宣言された範囲であって、実際に解決されたバージョンの組み合わせではないからです。

逆に言えば、pnpm update sharp だけで実際の欠陥は閉じます(警告は消えませんが)。

※ 余談ですが、pnpm の厳格な依存分離では sharp がプロジェクトから解決できず、ビルドが MissingSharp: Could not find Sharp. で止まりました。optionalDependencies は自動では引き上げられないため、sharp を直接の依存として書く必要があります。実運用のロックファイルで sharp のバージョンが固定されているのは、たいていこれが理由です。

05「直さない」という意味ではない

ここまでの結論は「この設定では到達できない」であって、「直さなくてよい」ではありません。この2つを混同すると、到達可能性の判断は言い訳の道具になります。

実際、Cの実験が示したとおり 設定1行で到達可能になりました。判断の寿命はその程度です。

到達不可と判断したときに、セットで決めること
いつ直すか
修正版が出ているなら次の定期ビルドで上げる。ただし更新の重さは案件ごとに違います。パッチ更新で済むものもあれば、メジャー更新が要るものもあります。「安いのだから全部すぐ上げればいい」で済むとは限りません。重いものほど、到達可能性の判断が効いてきます。「到達不可」は「今夜やるか、次の定期更新でよいか」を分けるための判断です。
何を残すか
判断した日付・根拠・実測値。「大丈夫だと思った」は半年後に誰も再現できません。この記事の表と、下に置いた再現手順がその形です。
いつ見直すか
許可リストを触ったとき。image.domains / remotePatterns への追記は、この判断を無効にします。設定変更のレビュー項目に入れておきます。

06測定器を2回取り違えた

この調査で自分が踏んだ失敗も書いておきます。表は正しく見えているのに、測っている対象が違っていました。

1回目:画像置き場が起動に失敗していたのに、結果は「正しく」出た
Cの実験では、許可リストに載せる「外部ホスト」役として、ローカルに小さなHTTPサーバーを立てています(第三者のサイトを叩かないため)。

初回、このサーバーが EADDRINUSE で起動に失敗していました。ところが同じポートに前の実験のプロセスが居残っていて応答してしまい、表には期待どおりの 200 image/webp が出ました。結論は偶然合っていましたが、測っていたのは別のサーバーです。

そこで、先へ進む前にそのポートが返すバイト列が自分の生成した画像と一致するかcmp で確かめ、違えば止まる処理を入れました。「応答があった=自分のサーバー」ではありません。
2回目:その対策の守備範囲が足りなかった
公開前の検証で、さらに2つ指摘されました。

ひとつは、守っていたのが画像置き場のポートだけで、Astro 側の3ポートは無防備だったこと。もうひとつは、後片付けに使っていた pkillGit Bash には存在せず|| true で握り潰されて一度も効いていなかったことです。

結果、実行後もサーバーが生きたまま残り、同じマシンで2回目を走らせると「ビルドし直した新しいサーバー」ではなく「前回のサーバー」を測っていました。1回目の対策で直したはずの失敗が、別のポートにそのまま残っていたわけです。

現在は、起動した子プロセスの PID を保持して kill し、ケースごとのビルドで固有の印を public/ に置いて、起動後にそれが返ってくるかを照合しています。ダミーのサーバーでポートを塞いで実行し、測定が中止されることまで確認しました。

もうひとつ。最新の @astrojs/node 11.1.6 を astro 7.2.2 と組むと、起動時に TypeError: app.getLogger is not a function で落ちます。再現環境は同時期に公開された版(11.1.2)で揃える必要がありました。しかも 11.1.6 の peerDependencies.astro^7.2.1 で、7.2.2 を許容しています。つまりパッケージマネージャは警告を出さないのに、起動時に落ちます。「最新を入れれば動く」も「peer が通れば大丈夫」も、古い版を再現したいときには成り立ちません。

07よくある質問

CVSS 9.8 が出たら、すぐ直さないとまずいのでは?
CVSS は「条件が揃ったときの深刻さ」の数値で、条件が自分の環境で揃っているかは含みません。勧告には必ず成立条件が書いてあるので、まずそれを読み、自分の構成で成立するかを実測してください。ただし到達できないことと、直さなくてよいことは別です(§05)。
Astro の /_image はどんなときに公開されますか?
全ページがプリレンダーされる構成では存在せず404prerender = false のルートを1本でも足すと現れ、引数なしで400を返します。この状態でも外部URLは403です。image.domains にホストを足すと、そのホストの画像だけ200で返ります(§02)。
Astro 7.2.8 では何が直ったのですか?
9.8 に関しては、sharp の下限が ^0.34.0 || ^0.35.0 から ^0.35.4 に上がっただけです。欠陥は sharp が使う libheif 側にあります。ただし 7.2.7→7.2.8 には画像と無関係な依存の差し替えが1件あり、「コードは1行も変わっていない」ではありません(§04)。
到達可能性で切ると見落としませんか?
見落とす可能性はあります。判断は「今の設定では」という限定つきで、測ったのも /_image の1経路だけです(§03)。だから①次の定期更新で上げる ②日付と根拠を残す ③設定変更時に再判定するの3点をセットにします(§05)。
CVSS の大きい順に対応すればよいですか?
点数の順は、刺さる順ではありません。同じ astro 7.2.2 の CVSS 6.3(認可バイパス)は、条件を満たす構成なら 9.8 より先に手を打つべきものです。点数ではなく、自分の構成で条件が成立するかで順番を決めてください(§01)。

データ・出典

  1. 実測データ:本記事の表の元データは、到達可能性9通り・依存範囲6版・OSV照会4件・公開日6件を記録した CSV(astro-image-reachability-2026-09-18.csv)です。数値はすべて下記の再現手順で再取得できます
  2. 再現手順:Node 20+ / pnpm / curl があれば、3構成のビルドと測定まで1コマンドで走るスクリプトを用意しました。外部サイトは一切叩きません(「外部ホスト」役もローカルに立て、素材のPNGもスクリプト内で生成します)。悪意ある AVIF の作成・投入は行いません
  3. GHSA-26w7-cxv4-gfx2(Astro・CVSS v3 9.8・2026-09-08公開)/GHSA-376h-93r7-7g6f(Astro・CVSS v4 6.3・同日)/GHSA-rgj7-g3m4-5g8c(sharp・同日)
  4. OSV APIPOST でパッケージとバージョンを渡すと該当件数が返ります・2026-09-18 照会)
  5. npm レジストリの astro メタデータ(依存範囲と公開日はここから。いずれも UTC:astro 7.2.2=2026-08-13、7.2.4=2026-08-19、7.2.8=2026-08-26。JSTだと 7.2.2 は 08-14、7.2.8 は 08-27 になります)
  6. 検証環境:Windows 11 / Node v22.22.0 / pnpm 10.34.4。astro 7.2.2・@astrojs/node 11.1.2・sharp 0.35.4
本記事は特定の製品やプロジェクトの安全性を保証するものではありません。到達可能性の判断は2026-09-18 時点の、本記事に書いた設定における実測に基づくもので、設定が変われば結論も変わります(実際、image.domains を1行足すだけで変わりました)。実測したのは GET /_image?href= の1経路のみで、remotePatternsgetImage() のサーバー側呼び出し・アプリ自身のアップロード処理・astro dev の挙動は測っていません。自分の環境での成立可否は、必ず自分で測ってください。また本記事は到達可能性の確認までを扱い、攻撃コードの作成・実行は行っていません。脆弱性は、到達可能かどうかにかかわらず、修正版が出ているなら計画的に適用することをおすすめします。