はじめに
Next.js(RSC含む)の脆弱性が公式からアナウンスされていたのに、気づくのが遅れてしまいました。毎日 Next.js の Blog やリリースノートを追い続けるのは、個人開発者にとってなかなか現実的ではないと思います。これをきっかけに、個人開発でも無理なく続けられるセキュリティ運用について改めて考えてみたので、備忘録としてまとめます。
今回の脆弱性について
今回の脆弱性対応のきっかけになった Cloudflare の公式情報が以下のページにまとまっています。
【開示された脆弱性の概要】
このページでは、React チームと Vercel から React Server Components(RSC)および Next.js に影響する複数の脆弱性が開示されたことが報告されています。脆弱性の種類は多岐にわたっており、サービス停止(DoS: 大量のリクエストを送りつけてサーバーを送り込み、サービスをダウンさせる攻撃)・ミドルウェアバイパス(認証をすり抜ける攻撃)・SSRF(サーバー側からの意図しないリクエストを発生させる攻撃)・XSS(クロスサイトスクリプティング: 悪意のあるコードをWebページに埋め込み、閲覧したユーザーのブラウザ上で実行させる攻撃)・キャッシュポイズニング(キャッシュに不正なデータを混入させる攻撃)などが含まれており、重大度も High から Low まで幅広い内容になっています。
脆弱性の数の多さと種類の広さを見て、「個人開発だから大丈夫」とは言い切れないと感じました。規模の大小にかかわらず、公開しているアプリである以上、攻撃対象になる可能性はゼロではないからです。
【Cloudflare WAF による緩和策について】
Cloudflare WAF(ウェブアプリケーションファイアウォール: Webアプリケーションへの通信を検査し、攻撃パターンに合致するリクエストを自動的に遮断する防御機能)の既存ルールが、今回開示された一部の DoS 脆弱性に対してすでにカバーを提供していることも報告されています。ただし、すべての脆弱性を WAF だけで防げるわけではなく、WAF での対応が難しい脆弱性も複数含まれています。
Cloudflare のページでも「WAF の緩和策だけに頼らず、アプリ側のバージョンアップを強く推奨する」と明記されています。つまり、Cloudflare を使っているからといって安心せず、アプリ自体をパッチ済みバージョンに更新することが根本的な対策になります。
基本コマンド一覧
まず、本記事で紹介する基本コマンドを一覧表にまとめました。各コマンドの詳細は後続のセクションで解説します。
| コマンド | 用途 | 使うタイミング |
|---|---|---|
yarn outdated / npm outdated
|
更新可能パッケージの確認 | 更新前の現状把握 |
yarn upgrade / npm update
|
パッケージの更新 | 同一メジャー内の定期更新 |
yarn audit / npm audit
|
脆弱性チェック | 更新後のセキュリティ確認 |
サポート対象バージョンに居続けることの大切さ
【サポート対象バージョンとは】
Next.js には、公式がセキュリティパッチや修正を提供してくれる「サポート対象バージョン」があります。大まかに言うと、現在積極的にメンテナンスされている最新に近いメジャーバージョンのことです。具体的には「Active(積極的にメンテナンスされている状態)」と「Maintenance(重大な修正のみ対応している状態)」の2段階があり、どちらもサポート対象に含まれます。
サポート対象外になったバージョンは、脆弱性が見つかっても修正パッチが提供されない可能性があります。つまり、古いバージョンを使い続けていると、既知の脆弱性がある状態で運用を続けることになりかねません。今回の脆弱性対応でも、サポート対象外のメジャーバージョンにはパッチが提供されていないことを確認しました。「古いバージョンのまま運用し続けると、問題が起きても自力で対応するしかない」という状況になりかねない点が、個人開発では特に怖いと感じています。
メジャーバージョンがサポート対象かどうかは、以下の公式ページで確認できます。
【同一メジャー内のパッチ・マイナー更新は比較的安全】
15.0.x → 15.5.x のような同一メジャー内の更新は、基本的に破壊的変更が少なく比較的安全に更新できると思います。一方、メジャーバージョンをまたぐ更新(例:15.x → 16.x)は破壊的変更が含まれることがあるため、公式のマイグレーションガイドを確認してから慎重に行うのが良さそうです。
この「同一メジャー内は比較的安全、メジャーをまたぐ場合は慎重に」という感覚を持っておくだけで、更新作業への心理的なハードルがかなり下がると感じています。毎回「壊れるかもしれない」と身構えてしまうと、更新自体が後回しになりがちです。同一メジャー内の定期更新は気軽に進めて、メジャーアップグレードだけ慎重に対応するというメリハリが、個人開発には合っていると思います。
同一メジャー内であっても、パッチバージョンにセキュリティ修正が含まれることは珍しくありません。むしろ、セキュリティ上の理由でパッチバージョンがリリースされるケースも多いので、同一メジャー内の更新を「大した変更じゃないから後でいい」と後回しにしないことが大切だと感じています。
【「今回だけ」が積み重なるリスク】
個人開発をしていると、「動いているからそのままでいいか」という判断をしがちだと思います。メジャーアップグレードには破壊的変更(既存の動作に影響する仕様変更)が伴うこともあり、腰が重くなるのは自然なことです。特に、本番環境で動いているアプリを壊したくないという心理から、「安定している今のバージョンを使い続ける」という選択をしてしまうことがあります。
ただ、サポート切れのバージョンを使い続けることは、「今回だけ」の判断が積み重なったリスクの先送りになります。動いているように見えても、セキュリティ上の問題が放置されている状態が続くわけです。
「いつかやろう」が積み重なって、気づいたらサポート対象外のバージョンをずっと使い続けていた、という状況は個人開発者あるあるだと思います。そうなる前に、定期的にバージョンを確認する習慣を作っておくことが、一番の予防策だと感じています。
【メジャーアップグレードのタイミングと進め方】
メジャーアップグレードは、以下の流れで対応するのが良さそうです。
- 新しいメジャーバージョンがリリースされたら、公式のマイグレーションガイドを確認する
-
npm outdated/yarn outdatedで現在のバージョン状況を把握する - ローカル環境でアップグレードし、動作確認してからコミットする
- エラーが出た場合はマイグレーションガイドの該当箇所を確認しながら対応する
メジャーアップグレードは一度に複数のメジャーをまたがないようにするのが良さそうです。たとえば2世代分古くなってしまっている場合は、一段階ずつ順を追って上げていく方が、問題の切り分けがしやすくなると思います。また、アップグレード後は yarn dev / npm run dev でローカル起動確認をしてから本番環境に反映するのが無難です。「手元では動いたから大丈夫」という判断は、本番環境での予期せぬエラーにつながりやすいので、できれば簡単な動作確認を一通りしてからコミットするのが良いと思います。
月1回の定期更新運用
【毎日監視は現実的ではない】
業務開発であれば、依存パッケージの更新情報を毎日チェックする運用も考えられます。ただ、個人開発は本業の合間に進めることが多く、毎日 Next.js の公式ブログやリリースノートを確認し続けるのは、正直かなり負担が大きいと感じます。
「監視の仕組みを作ること」自体がタスクになってしまい、肝心の開発が止まってしまうのは本末転倒です。セキュリティ対策はあくまで「開発を安全に続けるための土台」であって、それ自体がメインの作業になってしまうと本来やりたいことから遠ざかってしまいます。個人開発は継続することが一番大切だと思っているので、無理なく続けられる運用設計が重要だと感じています。
技術的な話をすると、AIを使ってリリースノートを自動監視し、更新があれば通知する仕組みを構築することは可能です。RSS(Webサイトの更新内容をプログラムから定期的に取得できるよう整形された配信データ) フィードを定期取得して AI に要約・判定させ、Slack やメールで通知するという構成で、実装例も存在します。ただ、この方法はリクエストのたびにトークン(AI の処理に使われる単位)を消費するため、API(プログラムから外部サービスの機能を利用するための取り決められた通信手順) の利用コストが継続的に発生します。コストを抑えたい個人開発の用途では、仕組みの構築・維持コストも含めると、費用対効果が見合わないケースが多いと思います。監視の自動化に時間とコストをかけるより、月1回手動で確認する運用の方が、個人開発には現実的だと感じています。
【月1回が個人開発にちょうど良い理由】
月1回であれば、たとえば「毎月第1週の土曜日に更新する」と決めておくだけで、無理なく継続できると思います。カレンダーにリマインダーを設定しておくだけでも、習慣として定着しやすくなります。スマートフォンのカレンダーアプリに繰り返しリマインダーを設定しておくのが、一番手軽な方法だと思います。
Next.js のパッチバージョン(セキュリティ修正を含むことが多い)は月に数回リリースされることもありますが、月1回の更新でも大半のセキュリティリスクはカバーできると考えています。業務開発ほどのリアルタイム性は個人開発では求められないことが多いですし、「忘れず定期的に続けること」の方が長期的には大切だと感じます。
また、月1回まとめて更新する習慣があれば、大きな脆弱性が発表されたタイミングで「先月更新したばかりだから最新に近い状態のはず」という安心感にもつながります。今回のように気づくのが遅れてしまう状況を防ぐためにも、定期的な更新サイクルを作っておくことが有効だと感じています。定期的に更新していれば、万が一脆弱性が発表されたときでも「最後に更新したのがいつか」がすぐにわかるので、影響範囲の把握もしやすくなります。
【更新作業の基本的な流れ】
更新のたびに手順を思い出す手間を減らすためにも、毎回同じ流れで作業するのが良さそうです。更新後は必ず以下の流れで動作確認をするのが良いと思います。
-
yarn outdated/npm outdatedで更新可能なパッケージを確認 -
yarn upgrade/npm updateで同一メジャー内の更新を実行 -
yarn audit/npm auditで脆弱性の残存チェック -
yarn dev/npm run devでローカル起動確認 - 問題がなければ
git commit
この流れを毎回同じ手順で行うことで、「更新したつもりが抜け漏れがあった」という事態を防ぎやすくなります。特に audit を省略してしまうと、更新後も脆弱性が残っているかどうかを確認できないまま進んでしまうことがあるので、セットで実行する習慣をつけておくと良いと思います。
yarn outdated / npm outdated を最初に実行するのは、「どのパッケージが更新対象か」を事前に把握するためです。更新前の状態を確認しておくと、更新後に何か問題が起きたときに「どのパッケージが変わったか」を特定しやすくなります。面倒に感じるかもしれませんが、トラブルシューティングの手がかりになるので、省略しないのが良さそうです。
yarn.lockはpackage.jsonとセットで更新
yarn upgrade を実行すると yarn.lock も更新されます。この2つはセットでコミットする必要があります。yarn.lock を更新しないまま放置すると、他の環境(チームメンバーのPCや本番環境など)で古いバージョンがインストールされてしまう可能性があります。個人開発でも、複数のマシンで作業している場合や、ホスティングサービスにデプロイしている場合は同じ問題が起きる可能性があります。
yarn.lock は自動生成されるファイルなので手動で編集する必要はありませんが、コミット対象からは外さないように注意が必要です。.gitignore に誤って追加してしまっていないか、一度確認しておくと安心です。
更新作業を自動化するスクリプト
月1更新を「やろうと思ったらすぐできる」状態にするために、更新作業をまとめたスクリプトを用意しておくと便利だと思います。毎回コマンドを1つずつ手で打つのは地味に手間がかかりますし、途中でコマンドを飛ばしてしまうリスクもあります。スクリプトにしておけば、ダブルクリック1回で3ステップをまとめて実行できます。
「スクリプトを作る」と聞くと難しそうに感じるかもしれませんが、今回紹介するものは定型コマンドを並べただけの単純な内容です。コピー&ペーストして使えるので、スクリプトを書いた経験がなくても問題なく使えると思います。
【3ステップの更新フロー】
スクリプトでは以下の3ステップを自動で実行します。
-
outdatedで更新可能なパッケージを確認(現状把握) -
upgrade/updateで同一メジャー内の更新を実行 -
auditで脆弱性が残っていないかチェック
スクリプト実行後は yarn dev / npm run dev でローカル起動を確認し、問題がなければ git commit するのが良さそうです。この「スクリプト実行 → 起動確認 → コミット」の流れをセットで習慣化しておくと、更新作業が月1ルーティンとして定着しやすくなると思います。
【Windows向け(.bat)】
.bat ファイルはダブルクリックで実行できます。プロジェクトのルートディレクトリに置いておくと、すぐ使えて便利です。ファイル名は update-deps.bat など、用途がわかりやすい名前にしておくのが良さそうです。
yarn版
@echo off
echo [1/3] 更新可能なパッケージを確認しています...
yarn outdated
echo [2/3] パッケージを更新しています...
yarn upgrade
echo [3/3] 脆弱性をチェックしています...
yarn audit
echo 完了しました。yarn dev で動作確認をしてください。
pause
npm版
@echo off
echo [1/3] 更新可能なパッケージを確認しています...
npm outdated
echo [2/3] パッケージを更新しています...
npm update
echo [3/3] 脆弱性をチェックしています...
npm audit
echo 完了しました。npm run dev で動作確認をしてください。
pause
末尾の pause は、コマンドプロンプトが実行後すぐ閉じないようにするためのものです。実行結果を確認してからウィンドウを閉じられるので、そのまま残しておくのが良いと思います。pause がないと、コマンドの実行結果を確認する前にウィンドウが閉じてしまい、エラーが出ていたかどうかもわからなくなってしまいます。
【Mac向け(.command)】
.command ファイルはダブルクリックで実行できます。ただし、後述の初回設定が必要です。.bat と同様に、プロジェクトのルートディレクトリに置いておくとすぐ使えて便利です。
yarn版
#!/bin/bash
echo "[1/3] 更新可能なパッケージを確認しています..."
yarn outdated
echo "[2/3] パッケージを更新しています..."
yarn upgrade
echo "[3/3] 脆弱性をチェックしています..."
yarn audit
echo "完了しました。yarn dev で動作確認をしてください。"
npm版
#!/bin/bash
echo "[1/3] 更新可能なパッケージを確認しています..."
npm outdated
echo "[2/3] パッケージを更新しています..."
npm update
echo "[3/3] 脆弱性をチェックしています..."
npm audit
echo "完了しました。npm run dev で動作確認をしてください。"
冒頭の #!/bin/bash は「このスクリプトを bash(シェルの一種)で実行する」という意味の記述です。Mac の .command ファイルでは先頭にこの1行を入れておくのが基本的な形式です。
まとめ
個人開発のセキュリティ運用は「完璧を目指す」より「サポート対象バージョンを維持しながら月1回継続する」ことが現実的だと感じています。今回の脆弱性対応で、気づくのが遅れて対応が後手に回った経験から、定期的な更新サイクルを作っておくことの大切さを改めて実感しました。自動化スクリプトをひとつ用意しておくだけで更新作業のハードルがぐっと下がるので、ぜひ活用してみてください。