依存の中の依存

pushしたら、GitHubからこう返ってきました。
remote: GitHub found 29 vulnerabilities on kisaku39/kisaku-portfolio's
remote: default branch (13 high, 14 moderate, 2 low).
29件。数字だけ見ると、手のつけようがないように感じます。ところが中身を分類すると、実質3つの問題でした。
警告の件数をそのまま受け取らずに読み替える方法、自動で出てくる更新PRの中で「そのまま入れてよいもの」の見分け方、そして「直接の依存を上げたのに警告が消えない」という現象の正体。
件数は、そのままの意味ではない
一覧を開いて、パッケージ別に数え直しました。
| パッケージ | 件数 | 用途 |
|---|---|---|
| next | 19 | 本番で動く |
| postcss | 5 | ビルド時に動く |
| lodash | 2 | 開発用のみ |
| glob | 1 | 開発用のみ |
| nanoid | 1 | 本番で動く |
29という数は、1つのパッケージに対する報告が積み上がった結果でした。nextの19件は、SSRF、Server ComponentsのDoS、画像最適化APIのDoS、キャッシュ混同——別々の問題ですが、直すのは「nextを上げる」の1手です。
29件を29個の作業だと思うと、着手する気力が湧きません。実際に必要な操作は5つ以下でした。警告システムは問題の個数を報告しますが、こちらが知りたいのは作業の個数です。ここを読み替えないと、量に圧倒されて何もしないことになります。
開発用かどうかで、優先度が変わる
一覧には Development という表示が付くものがあります。lodash と glob がそれでした。
これらはビルドや開発中にしか動かず、配信される成果物には入りません。つまり、サイトを見に来た人がこの脆弱性に触れることはありません。悪用するには、まず開発環境に入り込む必要があります。
危険がゼロという意味ではありませんが、本番で動くものと同じ緊急度で扱う必要はありません。3件がここに該当したので、後回しにしました。
自動で出てくるPRは、同じ安全さではない
Dependabotは修正用のPRを自動で作ってくれます。5本ありました。
#7 next 14.2.35 → 16.3.1 メジャー2つ跨ぎ
#5 eslint-config-next 14.2.35 → 16.3.1 メジャー2つ跨ぎ
#6 tailwindcss 3.4.4 → 4.3.3 メジャー跨ぎ
#8 graphql-request 6.1.0 → 7.4.0 メジャー跨ぎ
#10 postcss 8.5.23 → 8.5.26 パッチのみ
ボタンひとつでマージできます。しかし、同じ「更新」でも中身の危険度がまったく違います。
| バージョンの変化 | 意味 | 扱い |
|---|---|---|
| 8.5.23 → 8.5.26 | 不具合修正のみ。使い方は変わらない | そのまま入れてよい |
| 6.1.0 → 7.4.0 | 使い方が変わっている可能性がある | 動作確認が要る |
| 14.2.35 → 16.3.1 | 2世代ぶんの変更が入る | 移行作業として扱う |
手元で更新する場合は npm run build で通ることを確かめてからpushできます。PRをブラウザでマージすると、その工程が丸ごと飛びます。通るかどうかは、本番のデプロイが動き出してから分かります。パッチ更新なら許容できますが、メジャー更新でこれをやるのは賭けです。
そこで、#10だけをマージしました。チェックも2つとも通っていたので、これは安全な1本です。
1件しか減らなかった
マージ後、件数を数え直しました。
29件 → 28件
postcss 5件 → 4件
postcssを上げたのに、postcssの警告が4件残っています。バージョンは 8.5.26 になっているはずです。
残っている警告を1つ開くと、こう書いてありました。
影響を受けるバージョン 8.5.11 以下
修正されたバージョン 8.5.12
8.5.26 は 8.5.12 より新しいので、条件に当てはまりません。それなのに警告が残っている。
依存の中の依存
ロックファイルを直接見て、答えが出ました。
8.5.23 (dev) node_modules/postcss ← 8.5.26 に更新された
8.4.31 node_modules/next/node_modules/postcss ← 手つかず
postcssが2つ入っていました。
1つは自分で指定したもの。もう1つは next が自分の内側に抱えているものです。npmは、あるパッケージが要求するバージョンと自分の指定が食い違うとき、そのパッケージの中に別のコピーを置きます。同じ名前のものが、違うバージョンで同居できる仕組みです。
もし1つしか置けないなら、「AはpostCSS 8.4を要求、BはpostCSS 8.5を要求」という状況で、どちらかが壊れます。入れ子にすることで、互いの要求を同時に満たしています。便利な仕組みですが、その代わり「上げたつもりが上がっていない」が起こります。
自分で指定した依存は、自分で上げられます。他のパッケージが抱えている依存は、そのパッケージ自身が上げてくれるのを待つしかありません。
同じことが nanoid にも起きていました。これも next 経由で入っています。
読み替えた結果
ここまでを踏まえて、28件を整理し直しました。
| 分類 | 件数 | 必要な操作 |
|---|---|---|
| next 本体 | 18 | next を上げる(1手) |
| next が抱える postcss | 4 | |
| next が抱える nanoid | 1 | |
| 開発用(lodash / glob) | 5 | 本番に影響なし。後回し |
28件のうち23件が、1つの操作に集約されました。最初に見た「29件」という数字とは、受ける印象がまったく違います。
それでも、一気には上げない
では next を16に上げればいい——とはなりませんでした。
14から16は、2世代ぶんの変更が入ります。このサイトはApp Routerを使い、ISRで事前生成し、つい先ほどsitemapと構造化データを入れたばかりです。それらが動かなくなる可能性を、確認せずに本番へ出すことはできません。
段階を分けました。
npm install next@14 eslint-config-next@14 # 14系の最新まで
npm run build # 通るか確認
npm run start # 本番と同じ条件で表示確認
まずメジャーを跨がずに取れる分だけ取ります。これで消えない分が、16に上げる判断の材料になります。「全部消したい」から一気に上げると、落ちたときに何が原因か分かりません。
開発サーバー(npm run dev)と本番(npm run build → npm run start)では動きが違います。sitemapや構造化データのようにビルド時に生成されるものは、devで見ても本番と同じとは限りません。更新の影響を確かめる目的なら、本番と同じ手順で見る必要があります。
今回の学び
まず、警告の件数と、作業の件数は違うこと。29件のうち23件が1つの操作に集約されました。件数のまま受け取ると、量に圧倒されて着手できなくなります。パッケージ別に数え直すだけで、見える景色が変わります。
次に、「更新」の一言に、危険度の違う操作が混ざっていること。パッチ更新とメジャー更新は、ボタンの見た目が同じでもやっていることが違います。自動で提案されたからといって、同じ扱いをしてよいわけではありません。
そして、直接の依存を上げても、内側の依存は動かないこと。同じパッケージが違うバージョンで同居できる仕組みがあるため、「上げたのに消えない」が起こります。ロックファイルを見れば10秒で分かる話ですが、知らないと延々と悩みます。
最後に、本番に届くものと、届かないものを分けること。開発用の依存は、同じ「高」でも読者に届きません。全部を等しく緊急扱いすると、本当に急ぐものに手が回らなくなります。