脆弱性の告知から影響範囲を見積もる3つの問い

脆弱性の告知が同じ週に 3 件重なり、どこから直すかを決めた。「その機能を使っているか」で数えて漏らした見積もり、ホストの更新が届かないコンテナの CA、悪用の条件で付けた優先順位、直したつもりで古い版が残る場所を解説する。

脆弱性の告知から影響範囲を見積もる3つの問い

本記事はプロモーションを含みます。

はじめに: 「使っているか」で数えると漏れる

影響範囲は 3 つの問いで数える: 信頼できない入力を受けるか、どこで動くか、直しがどこまで届くか

2026 年 10 月の第 3 週、手元の複数のプロジェクトに効く脆弱性の告知が 3 件重なった。

  • 中間 CA (eMudhra) の弱い鍵。Go と Node.js がランタイム側で対応した
  • Node.js の 2026-10-21 のセキュリティリリース。Brotli の展開で範囲外書き込みが起きるものを含む
  • Next.js の 2026-10-14 のセキュリティ更新。Node.js の更新と同じ作業で片付いた

稼働中のもの、しばらく触っていないもの、手元の端末まで含めると、確かめる場所は十数か所になった。全部を同じ日に直すことはできないので、どれが本当に影響を受けるか、どの順で直すかを決める必要があった。

結論から書くと、影響範囲は次の 3 つの問いで数えると漏れにくい。

問い外したときに起きたこと
信頼できない相手のデータを受けるか「Brotli を自分で展開する所だけ」と見積もり、fetch() を使う所を数え漏らした
それはどこで動くか (本番か、ビルドだけか、手元か)ホストの OS を更新しても、コンテナの中の CA の一覧は変わらない
直しはどこまで届くか (ランタイム、イメージ、起動中のプロセス)版を上げても、起動中のプロセスや配布元が古い版を持ち続ける

最初の見積もりは 1 つ目の問いで外した。以下、外した見積もりと、数え直した手順を順に書く。

問い1: その機能を「使っているか」でなく「入力を受けるか」で数える

zlib を直接使っていなくても、fetch() が Content-Encoding: br を自動で展開するので Brotli の脆弱性の対象になる

Node.js の告知には、Brotli のデータを node:zlib で展開するアプリケーションに攻撃者がデータを渡せると、メモリを壊され、リモートコード実行につながりうると書かれていた。修正版は 22.23.4 / 24.22.1 / 26.12.1 だ。

最初、これを「zlib.brotliDecompress を自分で呼んでいる所だけ」と読み、緊急度を中程度に見積もった。ところが、標準の fetch() も対象だった。

fetch() は br の応答を自動で展開する

Node.js の fetch() (undici) は、応答の Content-Encoding が br / gzip / deflate なら展開してから返す。Node 24.22.1 で確かめた。

const r = await fetch("https://httpbin.org/brotli");
console.log(r.headers.get("content-encoding"), (await r.json()).brotli);
// => br true

コードに zlib の文字が無くても、展開は起きている。fetch の仕様は、ブラウザと同じく Content-Encoding の展開をクライアントの仕事にしているからだ。

数え直した基準

数える基準を「Brotli を自分で展開するか」から「信頼できない相手の HTTP 応答を受けるか」に替え、本番で Node が動くかで分ける

影響の有無を、「Brotli を自分で展開するか」から「信頼できない相手の HTTP 応答を受けるか」に替えて数え直した。

  • 対象: 外部の API を fetch() で叩く、Webhook の取得先やユーザが指定した URL を取りに行くサービス
  • 対象外: ビルドにだけ Node を使い、本番では Node が動かないもの

告知の文面は、脆弱なコードの場所 (Brotli のデコーダ) を書く。自分のコードのどこがその場所に届くかは、書かれていない。告知に出てくる API 名で grep して 0 件でも、標準ライブラリの中から呼ばれていれば対象になる。数えるときは、「攻撃者のデータがその場所まで届く経路があるか」を問う。

問い2: それはどこで動くか。コンテナにはホストの更新が届かない

Go は Linux で OS の CA ファイルを読む。コンテナではイメージに入ったファイルが使われ、ホストを更新しても変わらない

CA の件で最初に浮かんだ疑問は、「Go のサービスは、OS の CA を更新すれば直るのか」だった。

Go の crypto/x509 は、Linux では決まったパスの CA ファイルを順に探し、最初に見つかった 1 つを読む (root_linux.go の certFiles)。Debian / Ubuntu なら /etc/ssl/certs/ca-certificates.crt、Alpine なら /etc/ssl/cert.pem だ。

コンテナで動くサービスでは、このファイルはイメージの中にある。ホストの OS を更新しても、イメージの中の一覧は変わらない。CA の除去がサービスに届くのは、イメージを作り直したときだけだ。

さらに、言語の TLS ライブラリの多くは失効 (CRL / OCSP) を確かめない。CA が失効しても、ルートの一覧から外れるか、ランタイムが名指しで拒否するまで信頼し続ける。

ベースイメージを pull して実測した

ベースイメージを pull して CA の一覧を見ると、debian13 系と alpine:latest は外れ、debian12 系と版固定の alpine:3.21.3 は残っていた

「再ビルドすれば直る」が本当かは、イメージによって違う。推測で済ませず、使っているベースイメージを pull して、中の CA の一覧から問題の CA を探した。

img=gcr.io/distroless/static-debian12
docker pull -q "$img" >/dev/null
cid=$(docker create "$img" /x)
docker cp -L "$cid:/etc/ssl/certs/ca-certificates.crt" bundle.pem
docker rm "$cid" >/dev/null
openssl crl2pkcs7 -nocrl -certfile bundle.pem |
  openssl pkcs7 -print_certs -noout | grep '^subject=' | grep -i 'emSign'

結果は次のとおりだった。

ベースイメージ問題のルート
debian13 系の distroless外れた
node の alpine外れた
alpine:latest外れた
debian12 系の distroless残る
版を固定した alpine:3.21.3残る

タグを固定していないイメージは、再ビルドで直る。版を固定したイメージと古い系列のイメージは、再ビルドしても直らない。そこでは、ランタイムの更新が唯一の防御になる。

この調査は、ベースイメージの一覧と CA の検査をスクリプトにして、あとから再実行できるようにした。告知の続報が出たときや、ベースイメージが更新されたときに、同じ表を作り直せる。

ランタイムの拒否は、修正の出た系列にしか届かない

Go 1.26.10 / 1.27.3 は問題の CA を名指しで拒否する。サポートの切れた系列には修正が出ないので届かない

Go は 2026-10-19 に 1.27.3 と 1.26.10 を出し、crypto/x509 のセキュリティ修正として問題の中間 CA を名指しで拒否した。Node.js も同じ週のリリースで CA の一覧を更新している。

ランタイムの拒否は、イメージの CA の一覧に関係なく効く。ただし修正が出るのはサポート中の系列だけで、Go 1.25 以前のような古い版には届かない。版を固定したイメージの上で古い Go を使っているサービスは、どちらの防御も受けられない。

Go のバイナリが何の版でビルドされたかは、go version <バイナリ> で読める (go コマンドの説明)。配置済みのイメージを確かめると、ルートは既に外れていて、足りないのは Go の修正だけだった (4 つのバイナリがすべて go1.27.1)。

悪用の条件から優先順位を付ける

外から認証なしで攻撃できるもの => 手元の端末 => 稼働中のものは次のデプロイ => 古いものはまず稼働確認、の順に並べた

影響を受ける場所が分かっても、全部を同じ日には直せない。順番は、「攻撃者が何を用意すれば悪用できるか」で決めた。

CA の件を悪用するには、通信経路の乗っ取りが要る。乗っ取られると、サーバが ID トークンの検証のために取りに行く公開鍵をすり替えられ、任意の利用者になりすまされうる。ただし、Cloud Run から外への通信の経路を乗っ取るのは現実的でない。例外は、開発端末がつなぐ Wi-Fi と、取引先と結ぶ VPN の経路だった。

一方、Next.js の件は、外から認証なしで攻撃できる Critical を 2 件含んでいた。経路の乗っ取りも、ログインも要らない。

この差から、次の順に並べた。

優先対象理由
今週Next.js を使う 3 つのプロジェクト外から認証なしで攻撃できる
今週手元の端末経路を乗っ取られうる Wi-Fi につなぐ
次のデプロイ稼働中の Go の 4 つのプロジェクト外への通信の経路の乗っ取りは現実的でない
まず稼働確認最終コミットが古い 6 つのプロジェクト動いているかどうかで、要る作業が変わる

古いプロジェクトを外さず積んだのは、「動いていないはず」を確かめていなかったからだ。どれも各プロジェクトのタスク一覧に 1 件ずつ積み、一覧の無いプロジェクトには一覧を作った。

問い3: 直しはどこまで届くか。直したつもりで古い版が残る場所

版を上げても、起動中のプロセス、配布元のパッケージ、配置済みのバイナリに古い版が残る

版を上げる作業に入ってから、上げたのに古い版が使われ続ける場所が 3 つ見つかった。

起動中のプロセスは古いランタイムを持ち続ける

手元の Node.js を asdf で 24.21.0 から 24.22.1 に上げた。エディタ (Neovim) の中で Node を使うのは Copilot だけで、PATH の asdf の shim を通って起動する。

ところが、既に起動している Neovim の Copilot は、古い node のプロセスのまま動いている。shim を差し替えても、走っているプロセスは替わらない。Neovim を起動し直すか、:Copilot restart で子プロセスを起こし直して、初めて新しい版になる。

配布元のパッケージが修正版に追いついていない

Nix で環境を固定しているプロジェクトでは、nixpkgs を 2026-10-23 の版に上げても、nodejs_24 が修正前の版のままだった。追いつくのを待たず、修正の出ている nodejs_26 (26.12.1) に切り替えた。

「パッケージを最新にした」は「修正版になった」と同じではない。上げた後に、実際の版を修正版の番号と照らす。

配置済みのイメージはデプロイまで替わらない

Go の版を 1.27.3 に上げて main にマージしても、本番で動くのは前のデプロイのイメージだ。本番の CD が通った後に、イメージの中のバイナリが go1.27.3 になったことを確かめて、ようやく完了とした。

まとめ

  • 告知に出てくる API 名で数えない。「信頼できない相手のデータが、脆弱な場所に届く経路があるか」で数える。fetch() は br の応答を自動で展開するので、zlib を直接使っていなくても Brotli の脆弱性の対象になる
  • Go は Linux で OS の CA ファイルを読む。コンテナではイメージの中の一覧が使われ、ホストの更新は届かない。版を固定したイメージは再ビルドしても直らないので、ベースイメージを pull して確かめる
  • 優先順位は悪用の条件で決める。外から認証なしで攻撃できるものを先に、経路の乗っ取りが要るものは次のデプロイに回す
  • 版を上げた後に、起動中のプロセス、配布元のパッケージ、配置済みのバイナリの版を確かめる

コンテナのイメージがどう組み上がり、ベースイメージの更新がどこまで届くかを体系立てて押さえておくと、今回の「再ビルドで直るか」の判断が早くなる。

Docker/Kubernetes実践コンテナ開発入門 改訂新版 (山田明憲 著、技術評論社) 画像: 楽天市場

Docker/Kubernetes実践コンテナ開発入門 改訂新版 (山田明憲 著、技術評論社)

Go の http.Client が本番でだけ出すエラーの話は「Goで本番だけ出るconnection resetの直し方」に書いた。

参考