Joifup Blog

HTTP2Shell、自分のNode.jsは直った? 入れ方で違う確かめ方

2026-10-26

この記事は zenn.dev(新しいタブで開きます) にも掲載しています。

PHP の脆弱性(PHP2Shell)とほぼ同じ時期に、今度は HTTP2Shell(CVE-2026-111497)が出ました。毎日のように重大な脆弱性が出てくるな…と思いながら手元の Node.js を更新して、直ったか確かめようとしたら、見る場所によって「直った」とも「直っていない」とも読めて少し混乱しました。

この記事では、Node.js の入れ方ごとに直ったかをどこで確かめればいいかと、更新したあとも古いまま残りやすいものをまとめます。想定読者は、手元やサーバーで Node.js を使っていて、HTTP2Shell を見て「自分のは大丈夫か」を確かめたいエンジニアです。

調べる作業は Claude Code に任せました。公式のお知らせとアドバイザリを読ませてから手元の環境を調べさせて、出てきた結果を見て何を上げて何を止めるかを決めています。内容は 2026年10月26日時点のものです。

HTTP2Shell とは

名前から HTTP の欠陥かと思いましたが、中身は Google の圧縮ライブラリ Brotli の、展開(デコード)の処理の欠陥でした。対象は Brotli の 1.0.3〜1.2.0(修正版の 1.0.10・1.1.1 を除く)で、細工した圧縮データを展開させると、メモリが壊れて最悪の場合は外からコードを実行される可能性があります。

呼び名は PHP2Shell と同じ付け方で「HTTP から shell へ」、つまり HTTP の通信で届く圧縮データを入口に外からコマンドを実行されうる、と読めます。もとは Imperva が Node2Shell と名付けたもので、HTTP で圧縮データを自動で展開するライブラリやアプリにも広がることから Aikido が HTTP2Shell と呼んでいます。

深刻度は、CVE を登録した Google が CVSS 4.0 で 9.2(Critical)、Node.js 自身の評価は High です。Imperva が修正前の公式版 24.22.0(Linux arm64 の公式イメージ)で、Express 5 と fetch を使って外からコードを実行できることを、何度か試して再現したと報告しています(記事に攻撃のコードは載っていません)。実際に悪用されたという報告は見当たらず、CISA の KEV(悪用が確認された脆弱性の一覧)にも載っていません。シンガポールのサイバーセキュリティ庁(CSA)は Critical として、すぐに更新するよう呼びかけています。とくに Express 5 などで外に公開しているサーバーは、すぐに上げる対象です。日本の JPCERT/CC と IPA の注意喚起の一覧には、この時点では見当たりませんでした。

Node.js の HTTP サーバーそのものは、Brotli で圧縮されたリクエストを受け取っただけでは展開しません。展開するミドルウェアや fetch を通るときが対象になります。自分で展開の処理を書いていなくても、たとえば次のような経路で届きます。

  • Express 5 で express.json() などを使っていると、Brotli で圧縮したリクエストを送られただけで展開します。外に公開しているなら、誰でも送れます
  • Node.js の fetch は、相手のサーバーが Brotli で圧縮した応答(Content-Encoding: br)を返すと自動で展開します。webhook やリンクのプレビュー、クローラーのように、相手を選ばずに取りに行く処理が狙われやすいです

Node.js 以外では、Python の requests も brotli か brotlicffi が入っていると Brotli の応答を展開します。

主な修正版は次のとおりです。

対象修正版
Node.js22.23.4 / 24.22.1 / 26.12.1。20 以前と 23・25 系はサポートが終わっていて修正版が出ないので、このどれかの系列に上げる
Bun1.4.4(bun build --compile で作った実行ファイルも作り直す)
Pythonbrotli 1.2.1、brotlicffi 1.2.1.0
Brotli 本体1.0.10 / 1.1.1 / 1.2.1

Bun と Python は、この表の版以上になっているかで確かめられます。ややこしいのは Node.js です。ここからは Node.js の話をします。

直ったかの確かめ方は、入れ方で変わる

最初に混乱したのがここです。公式の Node.js を修正版の 24.22.1 にしたあと、中に入っている Brotli の版を process.versions.brotli で見ると、直す前と同じ 1.2.0 のままでした。Node.js のお知らせは Node.js 本体の版で修正版を示しています。本体の版は上がったのに中の Brotli の版は同じで、それで本当に直ってるの?と思いました。

整理すると、見ている数字が2つあります。Claude Code に公式版の 24.22.0 と 24.22.1 を落として並べさせた結果がこれです。

数字見方公式の 24.22.0(修正前)公式の 24.22.1(修正後)
Node.js 本体のバージョンnode -vv24.22.0v24.22.1
中に入っている Brotli のバージョンnode -p process.versions.brotli1.2.01.2.0

公式の Node.js は、Brotli を 1.2.1 に上げたのではなく、修正のコミットだけを取り込んでいます(v24.22.1 の CHANGELOG に deps: brotli: cherry-pick 0ed4268… とあります)。中身は直っていても、Brotli の版の番号は変わりません。

もう1つ話を分かりにくくしているのが、Node.js の入れ方によって Brotli を中に持っているかどうかが違うことです。

入れ方Brotli直ったかを見る場所
nodejs.org の配布物、それを落として使う nvm・fnm・Volta など、公式の Docker イメージ(alpine 版も含む)、NodeSource(apt で入れられる Node.js のビルドの配布元)Node.js の中に入っているNode.js 本体のバージョン(node -v)
Homebrewbrew で入れた brotli を読み込むbrew の brotli のバージョン(node -p process.versions.brotli)
Debian / Ubuntu(20.04 を除く)標準の aptOS の brotli のライブラリを読み込むlibbrotli1 のパッケージ
Alpine の apk で入れた nodejsOS の brotli のライブラリを読み込むbrotli-libs のパッケージ

どれで入れたか分からなければ、先に見分けます。行が出れば外の brotli を読み込んでいます。

# macOS
otool -L "$(node -p process.execPath)" | grep brotli
# Linux
ldd "$(node -p process.execPath)" | grep brotli

macOS の otool の行には current version 1.2.0 のように版が出ますが、これは node をビルドした時点の値で、今読み込んでいるものの版ではありません。版はこのあとの見方で確かめます。表に無い仕組み(Nix や MacPorts など)で入れていて行が出たら、行に出たパスのライブラリを入れた仕組みで brotli の版を確かめます。

行が出なければ、Node.js の中に Brotli が入っています。Node.js 本体のバージョンで確かめます。

node -v
# 22系は 22.23.4、24系は 24.22.1、26系は 26.12.1、それぞれ以上なら修正版

nvm や Volta などでプロジェクトごとに版を切り替えているなら、ホームディレクトリで見た版とプロジェクトで使っている版が違うことがあります。nvm ls・fnm list・volta list node などで、入っている版を全部見ておきます。

Homebrew の node は、brew の brotli を読み込む作りでした。この場合は process.versions.brotli が読み込んでいる brotli の版を出します。1.2.1 になっていれば直っています。逆に node 本体を新しくしても、brew の brotli が 1.2.0 のままなら直っていません。

Debian は、上流の版の番号を変えずに修正を当てます。版の番号を目で見比べると読み違えやすいので、修正が入った版以上かを判定させます。

# bookworm(trixie なら 1.1.0-2+deb13u1 と比べる)
dpkg --compare-versions "$(dpkg-query -W -f='${Version}' libbrotli1)" ge 1.0.9-2+deb12u1 && echo fixed || echo "not fixed (or not installed)"

Debian のセキュリティトラッカーを開くと nodejs の欄は vulnerable と出ます(重要度は unimportant)。nodejs は OS の brotli を使っていて、brotli 側が直っていれば大丈夫です。apt の nodejs は bookworm が 18 系、trixie が 20 系とサポートの終わった版ですが、この脆弱性に限っては libbrotli1 が直れば直ります。

Ubuntu は jammy(22.04)以降の nodejs を「対象外」としています。これは OS の brotli を使うためで、その brotli は 10月26日時点でまだ評価中で修正版が出ていません。修正版が出るまでは直っていない扱いです。focal(20.04)の nodejs は Brotli を中に持っていて、対象のままです。Ubuntu で急ぐなら、NodeSource か公式の配布物の修正版に切り替えるのが早いと思います。

Alpine の apk で入れた nodejs は apk list -I brotli-libs で brotli の版を見ます。Alpine 3.23 なら 1.2.1-r0 以上が修正版で、ほかの版の Alpine は Alpine のセキュリティ情報で確かめます。

確かめていて見つけた落とし穴:brotli を上げたら node が 26 系に

手元の Mac では、brew の node(24.10.0)が brew の brotli(1.2.0)を読み込んでいました。brew upgrade brotli で brotli を上げたら、brotli を使うものも一緒に上がって、node まで 24 系から 26 系に上がっていました。

brotli を上げれば、それ以降に起動する node は直ります。ただ、メジャーバージョンが変わるとほかのところで困ることがあるので、同じ 24 系の修正版に揃えるために node@24 に切り替えました。

brew install node@24
brew unlink node
brew link --overwrite --force --dry-run node@24   # 先に上書きされるファイルを見る
brew link --overwrite --force node@24
node -v                           # v24.22.1
node -p process.versions.brotli   # 1.2.1

--overwrite は、シンボリックリンクではない実ファイルも消して置き換えるので、dry-run の一覧は確かめてから実行します。戻すときは brew unlink node@24 && brew link node です。グローバルに入れたパッケージは 24 系と 26 系で共有されるので、26 系だった間に入れたネイティブモジュールは、24 系では動かないことがあります。

brew upgrade で brotli を使うものまでまとめて上がるのは、brew では普通の動きです。気づけたのは、上げたあとに node -v を見たからでした。

更新したあとも古いまま残るもの

Node.js や brotli を更新しても、すでに動いているものは古いまま動き続けます。

動いているプロセスは、起動したときの Node.js と brotli のまま動いています。brew や apt で brotli だけを上げた場合も同じで、起動し直すまで直りません。手元では、2週間以上動きっぱなしの開発用サーバー(next dev)や、Claude Code などの AI ツールが裏で npx から起動する補助のサーバー(MCP サーバー)が古いまま残っていました。MCP サーバーは、起動したツールを立ち上げ直すと新しい Node.js で起動し直します。

こうしたプロセスは表示名を npm exec … や next-server に書き換えるので、ps の結果を node で grep しても半分ほど漏れました。macOS では、実行ファイルと読み込んでいるライブラリから探すと漏れません。

# 実行ファイルが node のプロセスと、起動した日時
lsof -nP -d txt 2>/dev/null | LC_ALL=C awk '$NF ~ /\/node$/ {print $2}' | sort -u | xargs ps -o pid,lstart,comm -p
# brew の brotli を読み込んでいるプロセス(パスに版が出る。手元では全部 node でした)
lsof -nP -d txt 2>/dev/null | LC_ALL=C awk '/libbrotlidec/ {print $2, $NF}' | sort -u

更新した日時より前に起動したものが、古いままのプロセスです。手元では、2つ目のコマンドで brotli/1.2.0 のパスを読み込んだままの node がたくさん見つかりました。Linux では、次の2つで探せます。ほかのユーザーで動くサービスも見るので sudo を付けます。

# apt や apk の brotli を更新したあと、古いライブラリを読み込んだままのプロセス(/proc/<PID>/maps の PID)
sudo grep -l 'libbrotlidec.*(deleted)' /proc/[0-9]*/maps 2>/dev/null
# 動いている node の実行ファイル(nvm などはパスに版が出る)
sudo ls -l /proc/[0-9]*/exe 2>/dev/null | grep '/node$'

起動した日時は ps -o pid,lstart,comm -p <PID> で見られます。なお、ps でコマンドの引数まで出すと、MCP サーバーに渡している API キーなどが表示されることがあるので、出力を人に見せたり AI に貼ったりしないようにします。

コンテナのイメージには、ビルドした時点の Node.js が入ったままです。node:24.22.0 のように版を固定したタグは、作り直しても古いままです。固定していないタグでも、ベースイメージを取り直して作り直さないと、キャッシュの古いものが使われることがあります。

# 今動いているコンテナの Node.js の版
docker exec <コンテナ名> node -v
# ベースイメージを取り直して作り直す
docker build --pull -t <イメージ名> .

作り直したら、コンテナも作り直して起動し直します。イメージの中で apt や apk から Node.js を入れているなら、node -v ではなく上の表のとおりイメージの中の brotli のパッケージを見ます。この場合は apt や apk の層もキャッシュから使われることがあるので、--no-cache も付けて作り直します。

Node.js の単一実行ファイル(SEA)のように、ビルドに使った node を丸ごとコピーして1つのファイルにまとめたものも、作り直すまでは古いままです。

公開したままのサービスも残ります。手元では、前に自分で作った個人開発のミニ Web アプリが古いまま公開されていました。メンテもできていなかったので、この機会にクローズしました。止めるときに見落としやすいのが、自動でデプロイし直す仕組みです。Cloud Build のトリガーや GitHub Actions が残っていると、リポジトリに push したときに止めたはずのサービスがまた公開されます。サービスと一緒に止めておきます。

今回 Claude Code に確かめさせた流れ

  1. まず公式のお知らせやアドバイザリを読ませて、脆弱性の中身を理解させる
  2. 自分の環境で、Node.js などをどの方法で入れているかを洗い出させる。プロジェクトごとに版を切り替えているなら、入っている版を全部
  3. 入れ方ごとに、上の表の正しい場所で直ったかを確かめさせる
  4. 動いているプロセス、コンテナ、公開しているサービスまで見させる
  5. 何を上げて何を止めるかは自分で決めて、上げたあとは版を確かめ直し、動いているものを起動し直す

1 から 4 は読むだけのコマンドで済むので任せて、変えるコマンドは提案させてから自分で決めます。上げると決めたあとも、brew のように brotli を使うものまでまとめて変わることがあって、上げたあとに node -v と process.versions.brotli、動いているプロセスまで確かめ直しています。

まとめ

  • HTTP2Shell の中身は、Brotli の展開の処理の欠陥
  • 公式版の Node.js は、修正後も process.versions.brotli が 1.2.0 のまま。直ったかは Node.js 本体のバージョンで見る
  • brew や apt の Node.js は外の brotli を読み込むので、brotli 側を見る
  • 動いているプロセス、コンテナ、公開したままのサービスは、更新しても古いまま残る

AI で作って公開したサービスの棚卸し

最後に、エンジニアではない人に向けた話です。最近はバイブコーディングで、エンジニアではない人もサービスを作って公開することが増えています。AI に作ってもらったサービスだと、中で何が動いているかを細かく覚えていないこともあると思います。

使っている Node.js のバージョンや、どこに公開したかが分からないままだと、今回のような脆弱性が出ても自分が対象かどうかを確かめられません。作るのに使った AI に、使っている Node.js の版と公開先を聞いてみるのが手軽です。Vercel のようなホスティングを使っているなら、事業者の告知も確かめます。今は使っていないのに公開したままのものがあれば、一度洗い出しておくと、次にこういうことがあったときに慌てずに済みます。

参考:

おすすめ記事