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

本記事はプロモーションを含みます。
2026 年 10 月の第 3 週、手元の複数のプロジェクトに効く脆弱性の告知が 3 件重なった。
稼働中のもの、しばらく触っていないもの、手元の端末まで含めると、確かめる場所は十数か所になった。全部を同じ日に直すことはできないので、どれが本当に影響を受けるか、どの順で直すかを決める必要があった。
結論から書くと、影響範囲は次の 3 つの問いで数えると漏れにくい。
| 問い | 外したときに起きたこと |
|---|---|
| 信頼できない相手のデータを受けるか | 「Brotli を自分で展開する所だけ」と見積もり、fetch() を使う所を数え漏らした |
| それはどこで動くか (本番か、ビルドだけか、手元か) | ホストの OS を更新しても、コンテナの中の CA の一覧は変わらない |
| 直しはどこまで届くか (ランタイム、イメージ、起動中のプロセス) | 版を上げても、起動中のプロセスや配布元が古い版を持ち続ける |
最初の見積もりは 1 つ目の問いで外した。以下、外した見積もりと、数え直した手順を順に書く。
Node.js の告知には、Brotli のデータを node:zlib で展開するアプリケーションに攻撃者がデータを渡せると、メモリを壊され、リモートコード実行につながりうると書かれていた。修正版は 22.23.4 / 24.22.1 / 26.12.1 だ。
最初、これを「zlib.brotliDecompress を自分で呼んでいる所だけ」と読み、緊急度を中程度に見積もった。ところが、標準の fetch() も対象だった。
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 応答を受けるか」に替えて数え直した。
fetch() で叩く、Webhook の取得先やユーザが指定した URL を取りに行くサービス告知の文面は、脆弱なコードの場所 (Brotli のデコーダ) を書く。自分のコードのどこがその場所に届くかは、書かれていない。告知に出てくる API 名で grep して 0 件でも、標準ライブラリの中から呼ばれていれば対象になる。数えるときは、「攻撃者のデータがその場所まで届く経路があるか」を問う。
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 して、中の 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 は 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 つ見つかった。
手元の 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 になったことを確かめて、ようやく完了とした。
fetch() は br の応答を自動で展開するので、zlib を直接使っていなくても Brotli の脆弱性の対象になるコンテナのイメージがどう組み上がり、ベースイメージの更新がどこまで届くかを体系立てて押さえておくと、今回の「再ビルドで直るか」の判断が早くなる。
Go の http.Client が本番でだけ出すエラーの話は「Goで本番だけ出るconnection resetの直し方」に書いた。