2026年10月21日、Node.jsから22.x / 24.x / 26.xを対象にしたセキュリティリリースが公開されました。Brotliで圧縮されたデータを展開する処理で、リモートでコードを実行されるおそれがある脆弱性が含まれていました。
担当しているコーポレートサイト(Next.jsをコンテナにしてCloud Runで動かしている)について影響を調べ、対応しました。結論から言うと、追加の作業はほぼ要りませんでした。ただ、それを「確かめる」までにいくつか気づきがあったので、手順と判断の理由をまとめます。
この記事の内容は2026年10月時点のものです。担当しているサイトが特定できないよう、構成の細部は伏せています。
同じ日には、PHPの緊急脆弱性php2shellの修正版も公開されています。そちらへの対応は PHPの緊急脆弱性に、Xserver上のWordPressで一晩対応した記録 にまとめています。
この記事の要点
- Node.js本体(ランタイム)の脆弱性は、Dependabot alertsには出てこない。自分で拾いに行く必要がある
- Cloud Runで動いているコンテナのNodeの版は、ビルドログのベースイメージのdigestと、Docker Hubのdigestを照らし合わせれば、レジストリの権限がなくても確かめられる
- ベースイメージを
node:22-alpineのようにメジャー版だけで指定していたので、修正版の公開から1〜2日で、普段のデプロイによって自動的に修正版に入れ替わっていた - いちばん古いまま残っていたのは、ローカルの開発環境だった
1. 脆弱性の概要
公式のアドバイザリ(October 2026 Security Releases)によると、CVEで10件(High 4 / Medium 3 / Low 3)です。
| 深刻度 | 主な内容 | 対象 |
|---|---|---|
| High | Brotliのデコーダーの領域外書き込み(node:zlib)。クラッシュ、メモリの漏えい、リモートでのコード実行のおそれ |
22 / 24 / 26 |
| High | dns.resolve*()が、NULを含むホスト名を途中で切り詰める |
22 / 24 / 26 |
| High | IPアドレスの書き方の揺れ(先頭のゼロなど)で、TLSのホスト名の検証をすり抜けられる | 22 / 24 / 26 |
| High | 全角数字のIPアドレスで、TLSのホスト名の検証をすり抜けられる | 24 / 26(22は対象外) |
| Medium / Low | Keep-aliveのソケットで不正なレスポンスを受け付ける、スナップショットを使うと乱数が毎回同じになる、権限モデルの不備など | ― |
修正版は22.23.4 / 24.22.1 / 26.12.1です。サポートが終わったバージョン(20系以前、25系など)には修正版が出ないので、サポート中のバージョンへの移行が必要です。
Brotliの件は、「信頼できないBrotliのデータを展開する」と影響を受けます。Node標準のfetch(Undici)は、Content-Encoding: brのレスポンスを自動で展開します。外部のAPIからデータを取ってくるサーバーの処理や、同期のスクリプトがあるなら、他人事ではありません。
2. まず「Dependabotに出ていない」ことに気づく
最初にリポジトリのDependabot alertsを見ましたが、出ていたのは開発用のnpmパッケージのアラートだけで、今回のNode.jsの件とは関係のないものでした。
Dependabot alertsが見ているのは、package-lock.jsonなどのパッケージの依存だけで、Node.js本体やDockerのベースイメージは対象外です。dependabot.ymlにdockerを足しても、届くのは「新しいタグが出ました」というバージョン更新のプルリクエストで、CriticalやHighのような深刻度の付いたアラートにはなりません(GitHub Docs: supported ecosystems)。
ランタイムの脆弱性は、別の手段で拾う必要があります。
3. どこでNode.jsを使っているかを洗い出す
リポジトリの中で、Nodeの版を指定している箇所を探しました。
バージョンを指定するファイルを確かめます。
cat .nvmrc .node-version .tool-versions 2>/dev/null
package.jsonのenginesとvoltaを確かめます。
grep -n -A3 '"engines"\|"volta"' package.json
Dockerfileのベースイメージと、CIの設定を確かめます。
grep -n '^FROM' Dockerfile
grep -rn 'node-version' .github/workflows
結果は次のとおりです。
| 場所 | 指定 | 判断 |
|---|---|---|
| Dockerfile(3つのステージ) | node:22-alpine |
影響あり。動いているイメージがどの版かを確かめる必要がある |
| CI(GitHub Actions) | node-version: 22 |
実行のたびに最新の22.xを取ってくるので、次の実行から自動で修正版になる |
| ローカルの開発環境 | 各自の環境しだい | 確かめる必要がある |
4. Cloud Runで動いているNodeの版を確かめる
ここがいちばん悩んだところです。
素直な方法:イメージを取ってきてnode -v
docker run --rm --entrypoint node <レジストリ>/<イメージ>@sha256:<digest> -v
これがいちばん確実です。ただ、私の権限ではコンテナのレジストリ(Artifact Registry)からイメージを取得できず、downloadArtifacts deniedで失敗しました。運用の担当と開発の担当で権限が分かれている現場では、よくあることだと思います。
権限がなくてもできる方法:digestを照らし合わせる
発想を変えて、「ビルドしたときに、どのベースイメージが使われたか」を確かめることにしました。
手順1:ビルドログから、ベースイメージのdigestを拾う
GitHub Actionsのデプロイのジョブで、docker buildのログをnode:22-alpineで検索すると、BuildKitが次のような行を出しています。
#8 [deps 1/5] FROM docker.io/library/node:22-alpine@sha256:<digest>
node:22-alpineというタグが、ビルドした時点で どの実体(digest)を指していたか が記録されています。
手順2:修正版のdigestを調べる
Docker Hubは公開されているので、権限は要りません。
docker buildx imagetools inspect node:22.23.4-alpine | head -3
Name: docker.io/library/node:22.23.4-alpine
MediaType: application/vnd.oci.image.index.v1+json
Digest: sha256:<digest>
手順3:比べる
本番もステージングも、ビルドログのdigestとnode:22.23.4-alpineのdigestが一致しました。修正版のNode 22.23.4でビルドされたイメージが動いていることを確かめられました。
なぜ何もしなくても修正版になっていたのか
- デプロイはGitHub Actionsのランナーで、キャッシュを使わずに
docker buildしている → ビルドのたびにnode:22-alpineの最新版を取ってくる - デプロイの頻度が高く、修正版の公開後すぐに次のデプロイが走っていた
- Docker Hubの公式イメージは、Nodeのリリースから短い時間で更新される
この3つがそろっていたので、修正版の公開から1〜2日のうちに、普段のデプロイで自然に入れ替わっていました。
逆に言うと、しばらくデプロイしていないサービスは、node:22-alpineと書いていても古いまま動き続けます。Cloud Runは受け取ったイメージをそのまま動かすだけで、Nodeの版を自動で上げてはくれません。
5. ローカルの開発環境をそろえる
意外と見落としていたのがこちらです。私のMacにはVoltaでv24.14.1が入っていて、修正前の版でした。そのうえ、本番(22系)ともメジャー版が食い違っていました。
volta install node@22
node -v
npm ci && npm test
node -vがv22.23.4になり、テストもすべて通りました。
ローカルはインターネットに公開されたサーバーではありません。ただ、開発サーバーや同期のスクリプトは外と通信していますし、開発用のPCには認証情報や.envが集まっています。放っておかないほうがよいと判断し、開発メンバーにも 本番と同じ22系の最新版(v22.23.4以上)にそろえるよう共有しました。
細かい注意点もあわせて共有しています。
npm ciのあとに「npmの新しいメジャー版に上げられます」と出ても上げない(Node 22に付いているnpmのままにして、lockfileに余計な差分を出さない)npm audit fixをローカルで実行しない(依存パッケージの更新は、Dependabotのプルリクエストでまとめて行う)
6. ベースイメージは固定すべきか
調べている途中で、「node:22.23.4-alpineのように版を固定したほうがいいのでは」という話になったので、整理しました。
| 固定する(+Dependabotのdocker) | 固定しない(node:22-alpine) |
|
|---|---|---|
| 良いところ | 何が動いているかがコードを見れば分かる。同じコミットからは同じイメージができる。更新は必ずプルリクエストを通る | ビルドし直すだけで修正版が入る。運用の手間がない |
| 気をつけるところ | プルリクエストをマージするまで修正版が入らない。放っておくと古いまま止まる | 今何が動いているかは、ログを見ないと分からない。ベースイメージが知らないうちに変わる |
| 共通 | 脆弱性のアラートは出ない。デプロイし直さない限り、動いているコンテナは古いまま | 同左 |
このサイトはデプロイの頻度が高く、固定しないことの弱点も「ビルドログのdigest」「Cloud Runの前のリビジョンへの切り戻し」「ステージングでの事前の確認」で補えています。そのため、今は固定せずに運用を続けることにしました。デプロイの頻度が落ちてきたら、固定+Dependabotに切り替えるつもりです。
なお、どちらを選んでも、node:22-alpineが24系に勝手に上がることはありません。Node 22は2027年4月 でサポートが終わるので、それまでにDockerfile、CI、ローカルを、24系か26系へ手で移す必要があります。
7. 振り返り
- ランタイムの脆弱性は、自分で拾いに行く:Dependabot alertsは万能ではありません。GitHubの
nodejs/nodeのリポジトリのReleasesをWatchしておく、コンテナイメージのスキャンをCIに入れるなど、別の経路を用意しておくと安心です - 「動いているもの」を確かめる手段を持っておく:ビルドログのdigestとDocker Hubのdigestを照らし合わせる方法は、レジストリの権限がなくても使えて、次からもそのまま使い回せます
- ローカル環境も対象に入れる:本番だけを見ていると、開発用のPCがいちばん古いまま、ということが起きます
- 「何もしなくてよかった」も、確かめて初めて言える:結果的に作業はほぼ要りませんでしたが、確かめた手順と判断の理由を残しておけば、次に同じことが起きたときにすぐ判断できます
同じリリースをきっかけに、放っておいた個人サイトのNext.js 13も片付けました。そちらは Node.jsのセキュリティリリースをきっかけに、放置していたNext.js 13を片付けた話 にまとめています。
