技術約15分で読めます

Node.js以外に広がるBrotliの脆弱性を、PythonとGit for Windowsで確かめた

いけさん目次

TL;DR

影響 GoogleのBrotliライブラリ1.0.3〜1.2.0の展開処理に範囲外書き込み。Node.jsのほか、PythonのBrotli・brotlicffiパッケージ、Brotliを組み込んだcurl(--compressed を付けた場合)など、BrotliのCライブラリで展開するものが対象
対応 Brotli 1.0.10、1.1.1、1.2.1、Pythonは Brotli>=1.2.1、brotlicffi>=1.2.1.0。Node.jsは前回の記事のとおり22.23.4、24.22.1、26.12.1
手元の確認 Brotliパッケージ入りのPythonは Accept-Encoding: identity でも応答を展開。10月22日公開のGit for Windows 2.56.0.windows.3の同梱Brotliは1.2.0のまま。このPCのNode.jsは22.23.2から22.23.4へ入れ替え


前回の Node.js v24.22.1で直ったHIGH脆弱性4件を修正前と比べた で、Node.jsのBrotli展開時の範囲外書き込み(CVE-2026-111497)を扱った。
このときはNode.jsのセキュリティリリースの1件として見ていたが、バグはNode.jsが組み込んでいるGoogleのBrotliライブラリ側にある。
同じライブラリはPythonのパッケージやcurlにも入っているので、10月23日にはAikidoが「HTTPを使う多くのライブラリやアプリが影響を受ける」として、HTTP2Shellという名前を付けて注意を促した(Aikido — Node2Shell Brotli vuln (CVE-2026-111497) evolves to HTTP2Shell)。

Node.jsの外でどこまで影響があるのか、手元のWindows 11のPCで、Python、curl、Gitを順に確かめた。
外部のサーバーへの攻撃はしていない。どの条件でBrotliの展開処理に入るかは、正常なBrotliデータを返すローカルのHTTPサーバーを立てて調べた。
Node.jsの修正の確認だけは、修正コミットのテストに入っている不正なデータを手元で展開させている。

検証環境

項目内容
OSWindows 11 Home
Python3.14、urllib3 2.6.2、requests 2.32.5、修正前のbrotli 1.2.0、pip-audit 2.10.1
curlWindows標準の8.21.0と、Git for Windows同梱の8.22.0
GitGit for Windows 2.56.0.windows.2
Node.jsv22.23.2(途中でv22.23.4へ入れ替え)
確認した日時2026年10月24日13時〜14時(JST)

Node2Shell、HTTP2Shell、Broccoli2Shellという呼び名とバグの中身

CVE-2026-111497は、最初にNode.jsで公表されたことからNode2Shellと名付けられ、その後AikidoがHTTP2Shellと呼んだ。
影響範囲をまとめた Broccoli2Shell というページもあり、3つとも同じCVEを指している。
このページによると、報告者はOpenAIのCodex Securityのチームで、ページの作者は発見者とは別の人だという。

Brotliは、HTTPのレスポンスなどに使われる圧縮形式で、圧縮データは「ブロック」という単位に分かれ、ブロックごとに使う符号表を切り替えられる。
展開処理のうち、この切り替えを読む関数 DecodeBlockTypeAndLength() は、成功か失敗かの2つの値しか返していなかった。
失敗には「データが壊れている」と「続きの入力がまだ届いていない」の2つの意味があり、呼び出し側はどちらなのかを区別できない。
さらに、高速に処理するほうのコードでは戻り値そのものを確かめていなかった。

Aikidoの説明では、ブロックの種類が1つしかないと宣言したデータで、そのブロックを使い切らせてから切り替えを要求すると、成り立たない切り替えでも処理が止まらない。
展開処理は前のブロックの状態のまま続き、その先で確保した範囲の外に書き込む。
修正では、関数がエラーの種類を返すようにして、新しいエラー BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH(-17)で展開を止める。

この修正コミットは、Brotliのリポジトリに2026年2月27日に入っていた(Detect and report invalid stream earlier.)。
コミットメッセージは「不正な圧縮データを早めに検出して報告する」というだけで、セキュリティの修正だとは書かれていない。
修正を含むリリースは、10月21日に出たBrotli 1.0.10、1.1.1、1.2.1が最初になる(google/brotli Releases)。

脆弱性の深刻度は、CVSS(Common Vulnerability Scoring System)という0〜10の共通の採点基準で表すことが多い。
CVEを割り当てたGoogleは、CVSS 4.0で9.2(Critical、最も重い区分)とし、Node.jsは独自の基準でHIGHとしている(OSV — CVE-2026-111497)。
CVEの説明文には、認証なしのリモートの攻撃者が、メモリの破壊、メモリ内容の漏えい、サービス停止、場合によっては任意のコード実行(攻撃者が用意した処理を動かすこと)を起こせると書かれている。

コード実行まで確かめた例として、Impervaは公式のNode.js 24.22.0(Linux arm64)のコンテナイメージで、Express 5とbody-parser 2で作ったアプリに細工したBrotliデータを送り、アプリにない処理を実行させている(Imperva — Node2Shell: From Brotli Data to Remote Code Execution)。
毎回成功したわけではなく、何度か試す必要があったとされ、攻撃用のコードは公開していない。
Broccoli2Shellのページによると、10月22日時点で実際の攻撃は報告されていない。

Brotliの影響を受けるソフトと修正版

Brotliの展開処理を自前のコピーで持っているソフトは、それぞれが更新版を出すまで直らない。
nodejs.orgが配布するNode.jsもBrotliを本体に組み込んでいるので、OSのlibbrotliを更新しても直らない(Linuxのディストリビューションが独自にビルドしたNode.jsは、ビルドの設定によってOSのBrotliを使う)。
各所の資料で修正版を確認できたのは、次の表の6つ。

対象影響を受ける版修正版出典
Google Brotli(Cライブラリ)1.0.3〜1.2.01.0.10、1.1.1、1.2.1Brotli Releases
Node.js22.x、24.x、26.x の修正前22.23.4、24.22.1、26.12.1前回の記事
Python Brotli1.2.1より前1.2.1GHSA-9v8h-qqgm-4m35
Python brotlicffi1.2.1.0より前1.2.1.0GHSA-9v8h-qqgm-4m35
Bun修正前の版1.4.4Aikido
Envoy修正前の各バージョン系列1.40.0、1.39.3、1.38.6、1.37.8(1.36.12にも修正が入ったが、1.36系はサポート終了)Aikido

v25、v23、v20などサポートが終わったNode.jsには修正版が出ない。

Aikidoの投稿にある「まだすべてに修正が出ているわけではない」は、Brotliのコピーを持ったまま更新が出ていないソフトがあることを指している。
Broccoli2Shellのページは、10月24日時点で未修正のコピーを持つものとして、.NETのランタイム、PHPのbrotli拡張、Rubyのbrotli gem、DuckDB、ClickHouse、Apache Arrow、7-Zipの派生版などを挙げている。
一方、AikidoはApacheの標準の mod_brotli とnginxは、標準の設定ではBrotliを展開しない(圧縮して送るだけ)としている。

このPCのNode.jsを22.23.4へ入れ替えた

このPCのNode.jsはv22.23.2で、修正前の版だった。
wingetやnvmなどのパッケージ管理ソフトは使わずに、nodejs.orgのWindows用zipを展開したフォルダーへPATHを通して使っている。
npmで -g を付けてグローバルに入れたパッケージ(Codex CLI、Vercel CLIなど)も、同じフォルダーの node_modules に入っている。

nodejs.orgのNode.jsはBrotliを本体に組み込んでいるので、Windows版でも node.exe を修正版に替えれば、この脆弱性は直る。
zipに入っているのは node.exe と、同梱のnpm・corepack(pnpmやYarnを呼び出すための仕組み)、それらを起動するスクリプトだけなので、グローバルのパッケージを残したまま上書きできる。

まず修正版のzipを取ってきて、SHASUMS256.txt でハッシュを照合してから展開した。コマンドはGit Bashで実行している。

curl -LO https://nodejs.org/dist/v22.23.4/node-v22.23.4-win-x64.zip
curl -L https://nodejs.org/dist/v22.23.4/SHASUMS256.txt | grep " node-v22.23.4-win-x64.zip$" | sha256sum -c
unzip -q node-v22.23.4-win-x64.zip

動いている node.exe は上書きできないので、tasklist でNode.jsのプロセスがないことを確かめてから作業した。
戻せるように、今の node.exe と起動スクリプト、node_modules の中の npm と corepack のフォルダーを別の場所へ退避してから、npm と corepack のフォルダーを消した。
消さずに上書きすると新しい版でなくなったファイルが古いフォルダーに残るので、消したうえで、展開したフォルダーの中身をまるごとコピーした。

入れ替えの前後で、前回の記事と同じく、Node.jsの修正コミットのテストに入っている不正なBrotliデータ(ブロックの種類が1つなのに切り替えが現れるもの)を zlib.brotliDecompressSync() に渡した(src: expose BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH)。
このコミットでは、新しいエラーを表す定数 zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH も追加されたので、その値も調べた。

項目入れ替え前入れ替え後
node -vv22.23.2v22.23.4
process.versions.brotli1.1.01.1.0
zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCHundefined-17
不正なBrotliデータの展開ERR__ERROR_FORMAT_BLOCK_LENGTH_1ERR__ERROR_FORMAT_BLOCK_SWITCH
npm10.9.810.9.9
corepack0.34.60.36.0

入れ替えたあとは、不正なデータが修正で追加されたエラー ERR__ERROR_FORMAT_BLOCK_SWITCH で止まるようになったので、修正は入っている。
ただし process.versions.brotli の表示は 1.1.0 のまま変わらなかった。
Node.jsは同梱のBrotliを1.2.1へ上げたのではなく、1.1.0のソースにBrotli側の修正コミットだけを取り込んでいるため(CHANGELOG_V22)。
「Brotli 1.0.3〜1.2.0が影響を受ける」という範囲は、Google本家のBrotliをそのまま使う場合の話で、Node.jsの同梱分には当てはまらない。
Node.jsが修正済みかは、Brotliの版の表示ではなく、node -v が修正版か、修正で追加された定数があるかで確かめる。

node -p "process.version + ' ' + require('zlib').constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH"

修正前の版では定数が undefined で、修正版では -17 になる。
入れ替えたあとも、pnpm(corepack経由)、Codex CLI、Vercel CLIはそのまま起動した。

Pythonで応答が展開される条件

Pythonでよく使うHTTPクライアント(通信用のライブラリ)のurllib3は、requestsの内部でも使われている。
urllib3は、環境に Brotli か brotlicffi のパッケージが入っているときだけ、それを読み込んでBrotliの応答を展開する。
Aikidoによると、httpx(brotli のオプションを付けて入れた場合)、aiohttp、Scrapyも同じで、アプリのコードで import brotli をしていなくても、パッケージが入っているだけで展開処理に入る。

Python 3.14の仮想環境を2つ作り、片方にだけ修正前の brotli==1.2.0 を入れた。
ローカルに立てたHTTPサーバーは、リクエストの内容に関係なく、hello brotli をBrotliで圧縮して Content-Encoding: br を付けて返す。
Accept-Encoding は「この圧縮形式なら受け取れる」とサーバーに伝えるヘッダーで、identity は圧縮しないでほしいという意味になる。
decode_content=False は、urllib3に応答を展開しないよう指定するオプション。
どちらの環境もurllib3 2.6.2とrequests 2.32.5で、送った Accept-Encoding と受け取った本文を記録した。

送り方brotli 1.2.0ありbrotliなし
requestsの既定gzip, deflate, br, zstd を送り、展開したgzip, deflate, zstd を送り、展開しなかった
requestsで Accept-Encoding: identity を指定展開した展開しなかった
urllib3で Accept-Encoding: identity を指定展開した展開しなかった
urllib3で decode_content=False を指定展開しなかった展開しなかった

identity を送っても、Brotliパッケージが入っている環境では、サーバーが Content-Encoding: br を付けて返せば、urllib3は応答のヘッダーを見て展開した。
Node.jsの fetch() も、送ったヘッダーに関係なく応答のヘッダーで展開することを前回の記事で確かめていて、urllib3も fetch() も、送る側のヘッダーを変えるだけでは悪意のあるサーバーからの応答を防げない。

Aikidoは、外部のサーバーからデータを取ってくる処理が攻撃の入り口になるとして、Webhookの送信先、URLのプレビュー、クローラー(Webページを自動で巡回して取ってくるプログラム)のように、取りに行く先のURLを利用者や外部が決められるものを挙げている。

自分の環境に入っているかは pip show Brotli brotlicffi で確かめられ、どのパッケージがBrotliパッケージを依存として入れているかは pipdeptree -r -p Brotli で逆向きにたどれる。

pip show Brotli brotlicffi
pip install -U "Brotli>=1.2.1"
pip install -U "brotlicffi>=1.2.1.0"

PoetryやuvやPipenvでロックファイル(依存パッケージの版を固定するファイル)を使っている場合は、ロックファイルを更新してから、コンテナイメージなどもビルドし直す。

pip-auditでの検出

AikidoとPython側の影響を最初に分析した OpenSourceMalwareの記事 は、どちらも10月23日の時点で、脆弱性データベースにPyPIのパッケージが登録されていないので、依存関係のスキャナーでは検出できないと書いていた。
その後、GitHubのAdvisory Database(GHSA)のアドバイザリにはPyPIの brotli と brotlicffi が対象として加わり、GitHubのAPIで見るとレビュー日時は10月23日19時8分(UTC)になっている(GHSA-9v8h-qqgm-4m35)。
Pythonの脆弱性データベースにも PYSEC-2026-4322(Brotli)と PYSEC-2026-4323(brotlicffi)が入った。

先ほどのbrotli 1.2.0入りの仮想環境で、pip-audit 2.10.1を2026年10月24日13時12分(JST)に実行すると、brotliが検出された。
脆弱性情報の問い合わせ先によって登録の時期がずれることもあるので、既定の問い合わせ先(PyPI)と、-s osv で指定したOSVの両方で試し、どちらでも検出された。

Name     Version ID                  Fix Versions
-------- ------- ------------------- ------------
brotli   1.2.0   PYSEC-2026-4322     1.2.1
brotli   1.2.0   GHSA-9v8h-qqgm-4m35 1.2.1

AikidoとOpenSourceMalwareが書いていた「スキャナーで検出できない」状態は、この時点のpip-auditでは解消していた。

curlとGit for Windowsが同梱するBrotli

curlは、Brotliを組み込んでビルドされていれば、Brotliの応答を展開できる。
ただし、コマンドのcurlは --compressed を付けたときだけ展開し、付けなければどちらのcurlも圧縮されたまま出力した。
このPCには、Windowsに標準で入っているcurl(C:\Windows\System32\curl.exe)と、Git for Windowsに同梱されたcurlの2つがあり、curl -V の結果が違った。

curlcurl -V のBrotli送ったヘッダー結果
Windows標準 8.21.0なし--compressed のみエラー61(Unrecognized content encoding type)
Git for Windows同梱 8.22.0brotli/1.2.0--compressed のみ展開した
Git for Windows同梱 8.22.0brotli/1.2.0--compressed と -H "Accept-Encoding: identity"展開した

3行目では実際に identity を送っていたが、応答が Content-Encoding: br なので展開した。Pythonと同じで、送る側のヘッダーでは防げない。

curlを確かめたあと、Git自体の通信も調べた。
Gitは、HTTPSでの通信にこのcurlのライブラリ(libcurl)を使っていて、コマンドのcurlとは違い、--compressed のような指定がなくても br を受け入れるとサーバーに伝えている。
GIT_TRACE_CURL=1 を付けて、Pythonの確認で使ったローカルサーバー(常にBrotliで圧縮して返す)に git ls-remote を実行すると、Gitは Accept-Encoding: deflate, gzip, br, zstd を送り、Content-Encoding: br の応答を受け取っていた。
サーバーはGitのリポジトリではないので、そのあと not valid: is this a git repository? のエラーで止まった。
手元で確かめたのは、Gitが br を受け入れると伝えて br の応答を受け取るところまでになる。
そのあと展開されることは、Gitのソースから分かる。Gitの http.c は CURLOPT_ENCODING に空文字列を指定していて(git/http.c)、libcurlのドキュメントによると、この指定は組み込まれた圧縮形式をすべて Accept-Encoding に並べ、応答に Content-Encoding があれば展開する設定になる(CURLOPT_ACCEPT_ENCODING)。

このPCのGit for Windowsは2.56.0.windows.2で、同梱パッケージの一覧(C:\Program Files\Git\etc\package-versions.txt)には mingw-w64-ucrt-x86_64-brotli 1.2.0-1 とあった。
Git for Windowsの最新版は10月22日に公開された2.56.0.windows.3で、10月24日14時(JST)の時点でも変わっていない。
この版の配布物のうち、インストールせずに使える最小構成のMinGitを展開して同じ一覧を見ると、curlは8.23.0に上がっていたが、Brotliは 1.2.0-1 のままだった。
リリースノートにもBrotliやCVE-2026-111497の記述はない(Git for Windows Releases)。

最新版でもBrotliが古いままだったので、Git for Windowsのパッケージの元になっているMSYS2を調べると、10月22日にBrotliを1.2.1へ上げるコミットが入っていて、mingw-w64-ucrt-x86_64-brotli は 1.2.1-1 になっていた(MSYS2 Packages — mingw-w64-ucrt-x86_64-brotli)。
Git for Windowsがこれを取り込んだ版を出すまでは、信頼できないHTTPSのサーバーに git clone や git fetch で接続すると、修正前のBrotliで応答が展開される。

サーバー側でリクエストの本文を展開するExpress 5

サーバーが受け取るリクエストの本文(ボディ)も、Content-Encoding: br が付いていれば展開されることがある。
この場合は、攻撃者がリクエストを送るだけでよく、ログインも要らない。

Node.jsの http モジュールのサーバーは、受け取った本文を自分では展開しない。
Expressで本文を読む express.json() や express.raw() は、内部でbody-parser(リクエストの本文をJSONなどとして読み取るライブラリ)を使っている。
body-parserのREADMEには、すべてのパーサーが gzip、br、deflate、zstd の展開に対応していて、展開の可否を決める inflate オプションの既定値は true だと書かれている(expressjs/body-parser)。
冒頭で紹介したImpervaの実証も、Express 5とbody-parser 2の組み合わせだった。

Node.jsを修正版に上げれば、この経路も直る。
上げられない間は、本文を圧縮して送ってくる相手がいなければ、express.json({ inflate: false }) のように展開を止められる。
この指定では、圧縮された本文は展開せずにエラーで返す。

修正版を入れられない間の対処

修正版を入れられない間の対処として、AikidoとOpenSourceMalwareは、Brotliのデータを受け付けないか、展開そのものを止める方法を挙げている。

対象対処注意
リクエストを受け取るサーバーリバースプロキシ(アプリの手前でリクエストを中継するサーバー)やミドルウェアで、Content-Encoding に br を含むリクエストを拒否する大文字小文字の違いや、gzip, br のように複数並んだ場合も弾く
外部へ取りに行くクライアントBrotliの展開を無効にするか、展開が始まる前に応答の Content-Encoding を見て br を拒否するAccept-Encoding から br を外すだけでは足りない
PythonのクライアントBrotli と brotlicffi をアンインストールするBrotliでしか返さないサーバーとは通信できなくなる。httpx[brotli] のように入れ直してしまう依存がないかも調べる

Pythonで brotli を消した環境では、Content-Encoding: br の応答は展開されずに圧縮されたまま返ってきた。