Brotli脆弱性CVE-2026-111497とは — Node2ShellがPython・Bun・Envoyに広がった理由と対処

はじめに

10 月 21 日に Node.js のセキュリティリリースで修正された「Node2Shell」(CVE-2026-111497)は、Node.js 固有の不具合ではありませんでした。原因は Google の圧縮ライブラリ Brotli のデコーダにあり、それを使う Python の HTTP クライアント、Bun、Envoy、Istio、curl などにも影響が広がっています。Aikido はこれを「HTTP2Shell」と呼び直しました。CVSS v4.0 のスコアは 9.2(Critical)です。

注意したいのは、サーバーだけでなく、外部にリクエストを送るクライアント側も攻撃される点です。PHP2Shell(10/22 の記事)の直後で疲れている方も多いと思いますが、この記事では何が壊れているのか、なぜ影響がここまで広いのか、何を確認すればよいのかを順に整理します。

何が起きたのか

不具合の場所は、Brotli のデコーダで「ブロックの切り替え」を処理する DecodeBlockTypeAndLength() です。Brotli はストリームの途中で使うハフマン木を切り替えられ、その切り替えを読むのがこの関数です。

修正前のこの関数は、真偽値しか返しませんでした。BROTLI_FALSE は「ストリームが壊れている」と「入力がまだ足りない」の両方を意味し、呼び出し側は区別できません。さらに高速経路では戻り値そのものが捨てられていました。細工したストリームで切り替えを失敗させると、デコーダは古いブロック状態のまま処理を続け、範囲外への書き込みが起きます。結果はメモリの漏えい、プロセスのクラッシュ、任意コード実行のいずれかです。

修正版の対応は次のとおりです。

対象影響を受ける版修正版
libbrotli(upstream)1.0.3〜1.2.01.2.1(旧系列は 1.0.10 / 1.1.1)
Python Brotli1.2.0 以前1.2.1
Python brotlicffi1.2.0.x1.2.1.0
Node.js22 / 24 / 26 系22.23.4 / 24.22.1 / 26.12.1
Bun—1.4.4
Envoy—1.40.0 / 1.39.3 / 1.38.6 / 1.37.8
Istio—1.29.9 / 1.30.6 / 1.31.2

Node.js 20 以前と 23・25 系はサポートが終わっているため、修正版は出ません。upstream の修正(コミット 0ed4268)は、関数がエラーコードを返すように変え、新しいエラー BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH を追加するものです。Aikido によると、1.2.0 と 1.2.1 の差はこの修正とバージョン番号だけです。

経緯も少し変わっています。Aikido は、OpenAI が 2 月にこの不具合を見つけ、根本原因は Brotli のソース上ではその時点で直っていたものの、セキュリティ問題と認識されずリリースに入らなかったと書いています(HN では、この経緯の読み方に異論も出ています)。野生での悪用は 10/22 時点で報告されていませんが、研究者は改変していない Node.js 24.22.0 でコード実行を再現しています。

なぜここまで影響が広いのか

「使っているつもりがない」ライブラリが自動で使う

影響が広い一番の理由は、Brotli が任意の依存として自動検出されることです。Python では次のように動きます。

  • urllib3(つまり requests): Brotli か brotlicffi が入っていれば、自動で Brotli 応答を解凍する
  • httpx: httpx[brotli] のエクストラを入れていれば使う
  • aiohttp: Brotli が入っていれば使う
  • Scrapy など、これらの上に作られたツールも同様

つまりアプリのコードで import brotli をしていなくても、何かの依存が Brotli を引き込んでいれば攻撃面になります。HN では「自分の環境を調べたら入っていなかった」という報告もあり、入っているかどうかは環境ごとに確かめるしかないというのが実情です。

サーバーからの応答で攻撃される

もう一つの理由は、攻撃が双方向に成立することです。

  • 受信側(サーバー): Content-Encoding: br 付きのリクエストボディを解凍するサーバー。Node.js では Express 5 と body-parser 2.x がこれに当たり、express.json() も対象です。素の Node.js の HTTP サーバーは受信ボディを自分では解凍しません
  • 送信側(クライアント): 攻撃者が用意した、または乗っ取ったサーバーに接続するクライアント。Webhook の送信、URL プレビュー、クローラー、「この URL から画像を取り込む」機能などが典型です。Node.js の fetch() も Brotli 応答を自動で解凍します

ここで効かないのが Accept-Encoding から br を外す対策です。urllib3 は応答の Content-Encoding を見て解凍方法を決めるため、サーバーが勝手に Brotli で返してくれば解凍が走ります。リクエストヘッダで「Brotli は要らない」と伝えても、悪意あるサーバーがそれに従う理由はありません。

既存のスキャナーが見つけてくれない

10/23 時点では、CVE レコード・OSV・GitHub Advisory Database が PyPI パッケージとの対応を持っていませんでした。そのため pip-audit・Dependabot・Safety は脆弱な版を検出しない可能性があります。GitHub のアドバイザリ(GHSA-9v8h-qqgm-4m35)は出ており、対応付けは進んでいる途中です。「スキャンが緑だから大丈夫」とは、少なくとも今は言えません。

エンジニアへの影響と対処

まず確認すること

# Node.js(手元・CI・コンテナの中すべて)
node --version
docker run --rm <image> node --version

# Python
pip show Brotli brotlicffi
pipdeptree -r -p Brotli   # 何が Brotli を引き込んでいるか

ロックファイル(poetry.lock / uv.lock / requirements.txt)を brotli で検索するのも有効です。コンテナイメージは再ビルドしない限り古い版のままです。node:24.22.0 のように細かい版で固定したタグも同じです。

すぐ更新できない場合の暫定策

  • Python: Brotli と brotlicffi(httpx[brotli] などのエクストラを含む)をアンインストールし、ロックを作り直して再起動する。HTTP クライアントは gzip / deflate に戻ります。ただし Brotli で返してくるサーバーとの通信が失敗する可能性はあります
  • 受信側: リバースプロキシやロードバランサー、ミドルウェアで、Content-Encoding に br を含むリクエストを解凍前に拒否する(大文字小文字や、複数指定の中に混ざる場合も含める)
  • 送信側: Content-Encoding: br の応答を、自動解凍の前に拒否する。解凍後に調べても手遅れです

受信側の遮断は、送信側の穴を塞ぎません。根本的な対策は更新だけです。

優先順位の付け方

すべてを一斉に直せないなら、外部の URL にアクセスするサービスと圧縮されたリクエストボディを受け取るサービスから手を付けるのが妥当です。社内の決まった API にしか接続しないバッチは相対的に後回しにできます。一方で、ユーザーが入力した URL を取りに行く機能は、攻撃者がサーバーを選べるので最優先です。

個人開発でも見落としやすいのが、定期実行のスクリプトです。Reddit や RSS を取りに行く Python スクリプトが requests を使い、どこかの依存が Brotli を入れていれば対象になります。Cloud Functions などのマネージド環境は、ランタイムの Node.js の版を事業者に確認しておく必要があります。

まとめ

  • CVE-2026-111497 は Brotli デコーダのブロック切り替え処理で起きる範囲外書き込みで、CVSS v4.0 は 9.2
  • 影響は Node.js だけでなく、requests / httpx / aiohttp、Bun、Envoy、Istio、curl に広がる
  • サーバーからの応答だけで攻撃が成立するため、Accept-Encoding の調整では防げない
  • 現時点では pip-audit や Dependabot が検出しない場合があり、手で確認する必要がある

PHP2Shell に続き、広く使われる基盤ライブラリで重大な脆弱性が短い間隔で見つかっています。依存の「任意機能」が知らないうちに攻撃面を広げていないか、この機会に棚卸ししておくと次の脆弱性にも速く動けます。

ソース