01勧告には成立条件が書いてある
GHSA-26w7-cxv4-gfx2(2026-09-08公開・CVSS v3 9.8)の本文は短く、要点は2文です。
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 は「条件が揃ったときの深刻さ」の数値です。「その条件が自分の環境で揃っているか」は、点数のどこにも入っていません。だから点数を見ただけでは、直す順番すら決められません。
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 の最小構成を手元に立て、設定だけを変えて比較しました。
全ページ prerender
prerender = false のルートを1本だけ追加image.domains に外部ホストを1つ許可| 構成 | /_image(引数なし) | 外部URL 許可していないホスト | 外部URL 許可したホスト |
|---|---|---|---|
| A 全ページ prerender | 404 | 404 | 404 |
| B オンデマンド1本 | 400 | 403 | 403 |
C B + image.domains | 400 | 403 | 200 image/webp |
- A:完全に静的なサイトには
/_imageが存在しないnode アダプタを積んでいても、全ページがプリレンダーされるならエンドポイントごと生えません。画像はビルド時に変換済みで、実行時の変換口がありません。 - B:オンデマンドのルートを1本持つと
/_imageが現れるただし外部URLは 403 Forbidden。プロジェクト内の画像しか受け付けません。攻撃者が任意のAVIFを渡す経路がない=9.8 の条件は成立しません。 - C:許可リストに足したホストの画像は、取りに行って処理する
image.domainsに1行足しただけで 200image/webpが返りました。設定1行で到達可能になるということです。
image.domains や remotePatterns に載せた先が攻撃者の影響下にあるなら、そこが untrusted 入力の入口になります。具体的には 利用者がアップロードした画像を置くバケット、ユーザー投稿を配信するCDN、外部の画像プロキシ。「自社のドメインだから安全」とは限りません。そのホストに誰が書き込めるかで決まります。
※ この実験で測ったのは到達可能性だけです。悪意ある AVIF の作成・投入は行っていません。sharp も修正版の 0.35.4 を明示的に入れており、脆弱な 0.35.3 は動かしていません。「入口が開いているか」と「入れたら何が起きるか」は別の作業で、後者は攻撃コードの実行にあたるため、自分の環境であっても踏み込んでいません。また外部サイトは一切叩かず、「外部ホスト」の役もローカルに立てた小さなサーバーで代用しています。
03この測り方がカバーしないもの
結論を持ち帰る前に、測っていない範囲を先に書いておきます。ここを飛ばすと、この手法はただの言い訳になります。
GET /_image?href= のみです。remotePatterns による許可(試したのは image.domains だけ)、getImage() をサーバー側コードから呼ぶ経路、astro dev(開発サーバー)の挙動は測っていません。/_image がいくら閉じていても関係ありません。04これは「Astroの脆弱性」ではなかった
再現環境を作る過程で、勧告の構造が見えました。npm レジストリで astro の依存指定を並べると一目瞭然です。
| astro | sharp の指定 |
|---|---|
| 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.js の find-process → find-proc という依存の差し替え(開発サーバーのロック処理で、画像とは無関係)です。変わっていないのは画像処理まわりであって、コード全体ではありません。
ここから、直感に反する状態が生まれます。OSV に問い合わせた結果です。
curl -s -X POST -d '{"package":{"name":"astro","ecosystem":"npm"},"version":"7.2.2"}' https://api.osv.dev/v1/query^0.34.0 || ^0.35.0 は 0.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回取り違えた
この調査で自分が踏んだ失敗も書いておきます。表は正しく見えているのに、測っている対象が違っていました。
初回、このサーバーが
EADDRINUSE で起動に失敗していました。ところが同じポートに前の実験のプロセスが居残っていて応答してしまい、表には期待どおりの 200 image/webp が出ました。結論は偶然合っていましたが、測っていたのは別のサーバーです。そこで、先へ進む前にそのポートが返すバイト列が自分の生成した画像と一致するかを
cmp で確かめ、違えば止まる処理を入れました。「応答があった=自分のサーバー」ではありません。
ひとつは、守っていたのが画像置き場のポートだけで、Astro 側の3ポートは無防備だったこと。もうひとつは、後片付けに使っていた
pkill が Git 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よくある質問
/_image はどんなときに公開されますか?prerender = false のルートを1本でも足すと現れ、引数なしで400を返します。この状態でも外部URLは403です。image.domains にホストを足すと、そのホストの画像だけ200で返ります(§02)。^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)。データ・出典
- 実測データ:本記事の表の元データは、到達可能性9通り・依存範囲6版・OSV照会4件・公開日6件を記録した CSV(
astro-image-reachability-2026-09-18.csv)です。数値はすべて下記の再現手順で再取得できます - 再現手順:Node 20+ / pnpm / curl があれば、3構成のビルドと測定まで1コマンドで走るスクリプトを用意しました。外部サイトは一切叩きません(「外部ホスト」役もローカルに立て、素材のPNGもスクリプト内で生成します)。悪意ある AVIF の作成・投入は行いません
- 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・同日)
- OSV API(
POSTでパッケージとバージョンを渡すと該当件数が返ります・2026-09-18 照会) - 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 になります)
- 検証環境:Windows 11 / Node v22.22.0 / pnpm 10.34.4。astro 7.2.2・@astrojs/node 11.1.2・sharp 0.35.4
image.domains を1行足すだけで変わりました)。実測したのは GET /_image?href= の1経路のみで、remotePatterns・getImage() のサーバー側呼び出し・アプリ自身のアップロード処理・astro dev の挙動は測っていません。自分の環境での成立可否は、必ず自分で測ってください。また本記事は到達可能性の確認までを扱い、攻撃コードの作成・実行は行っていません。脆弱性は、到達可能かどうかにかかわらず、修正版が出ているなら計画的に適用することをおすすめします。