Node.jsのBrotli解凍の脆弱性CVE-2026-111497の仕組みを理解する:コードに書かなくても解凍は起きる

Node.jsのBrotli解凍の脆弱性CVE-2026-111497の仕組みを理解する:コードに書かなくても解凍は起きる

Node.js 22/24/26に同梱されたBrotliのデコーダーには、不正な圧縮データを「入力待ち」と誤判定し、内部バッファの外へ書き込む問題がありました。修正コミットから仕組みを読み解き、fetchやボディパーサーが自動で解凍する経路と、影響の確かめ方を整理します。
2026.11.02

どうも!オペ部の西村祐二です!

2026年10月21日、Node.js 22 / 24 / 26 のセキュリティリリースが公開されました。含まれる10件のうち、CVE-2026-111497 は Brotli のデコーダーで起きる範囲外書き込みです。細工した圧縮データを解凍させると、プロセスのクラッシュやメモリの漏えいにつながります。Imperva は、公式 Node.js 24.22.0 の Linux arm64 イメージでリモートコード実行も実証しています。原因は Google の Brotli ライブラリにあり、同じライブラリを使う Python の Brotli パッケージやプロキシの Envoy にも修正版が出ています。10月27日には米国の CISA が、この脆弱性を KEV(悪用が確認された脆弱性のカタログ)に追加しました。

アプリのコードで Brotli の解凍を呼んでいなくても、外部の URL を fetch() する機能や、圧縮された要求ボディを受け付ける Express 5 の API があれば対象になりえます。この記事では、上流の修正コミットを読み、デコーダーが不正なデータを「入力待ち」と誤判定して内部バッファの外へ書き込む仕組みを整理します。あわせて、アプリのコードの外で解凍が起きる経路、自分の環境が影響を受けるかどうかの確かめ方、修正版と暫定策をまとめます。

先に結論:仕組みの要点と確認・対策

CVE-2026-111497 は、Brotli のデコーダーが不正なブロック切り替えを「入力待ち」と誤判定し、8 バイトの内部バッファへの追記を続けて、バッファの外へ書き込む問題です。修正前のコードでは、形式エラーと入力不足が同じ戻り値(BROTLI_FALSE)になっていました。Node.js では、組み込みの fetch() や Express 5 / body-parser 2.x のパーサーがこの解凍をデフォルトで行うため、アプリのコードで Brotli を使っていなくても影響を受けます。

確認すること 対策
実行中の Node.js が 22.23.4 / 24.22.1 / 26.12.1 より古いか(node -v) 各系列の修正版以上へ更新する(2026年11月2日時点の各系列の最新は 22.23.4 / 24.22.2 / 26.13.0)。サポート終了の 20.x 以前には修正版が出ないため、サポート中の系列へ移る
コンテナイメージ、CI のランナー、単一実行ファイルに古い Node.js が入っていないか ベースイメージを更新して作り直し、再起動後に新しいランタイムが動いていることを確かめる
Ubuntu(22.04 以降)や Debian の公式リポジトリから nodejs パッケージを入れていないか この nodejs パッケージはシステムの brotli ライブラリを使うため、brotli のパッケージ(libbrotli1)を各ディストリビューションの修正版へ更新する
AWS Lambda の Node.js ランタイムで動く関数があるか ログの INIT_START に出るランタイムバージョンが、Node.js 22 は nodejs:22.mainline.v157、Node.js 24 は nodejs:24.mainline.v114 以上かを確かめる。Node.js 24 は process.version では判定できない。Node.js 18 / 20 のランタイムには修正が出ない
Python 環境に Brotli 1.2.1 未満、brotlicffi 1.2.1.0 未満(いずれも 1.0.3 以降)が入っていないか Brotli 1.2.1 / brotlicffi 1.2.1.0 以上へ更新する
Envoy で Brotli の decompressor を設定していないか v1.40.0 / v1.39.3 / v1.38.6 / v1.37.8 / v1.36.12 以上へ更新する
外部の URL を fetch() する機能や、圧縮された要求ボディを受け付ける API があるか 外部の URL を取得する機能は、この記事で紹介する暫定策(inflate の無効化、WAF)では守れないため、先に更新する。圧縮された要求ボディが不要な API は、更新までボディパーサーで圧縮ボディを拒否する

何が起きたのか

Node.js の10月セキュリティリリースは 10月21日に公開され、22.23.4、24.22.1、26.12.1 で10件の脆弱性が修正されました。2026年11月2日時点で、Node.js の脆弱性情報の一覧にこれより新しいセキュリティリリースは出ていません。10月21日以降に出た 24.22.2 と 26.13.0(「先に結論」の表の「最新」)は、どちらもセキュリティリリースではありません。26.13.0 は 26.x を LTS に移すリリースで、リリースノートによると、26.12.1 からの変更はメタデータの更新だけです。24.22.2 については、AWS が 24.22.1 に既知のリグレッションがあったとして、Lambda で 24.22.2 への移行を進めています(後述)。

このリリースで修正された CVE-2026-111497 は、次のように説明されています。

A flaw in the Brotli decoder bundled with Node.js allows malformed compressed input to write past the end of an internal decoder buffer.

Node.js に同梱された Brotli のデコーダーでは、不正な圧縮データを渡すと内部のバッファの末尾を越えて書き込まれる、という内容です。報告者は OpenAI Codex Security の Khai Tran 氏と Sylvie Mayer 氏で、Node.js の Matteo Collina 氏と Antoine du Hamel 氏が修正しています。Node.js のアドバイザリは、サポート終了済みの系列が常に影響を受けるとしています。

問題は Node.js だけのものではありません。GitHub Advisory Database の GHSA-9v8h-qqgm-4m35 によると、原因は Google の Brotli 1.0.3〜1.2.0 のデコーダー(c/dec/decode.c)です。各系列の修正版は 1.0.10 / 1.1.1 / 1.2.1 です。同じアドバイザリには、PyPI の Brotli(1.0.3 以上 1.2.1 未満が対象)と brotlicffi(1.0.3 以上 1.2.1.0 未満が対象)も登録されています。

Envoy のアドバイザリでは、影響を受けるのは、Brotli の decompressor を設定した構成の v1.39.2 / v1.38.5 / v1.37.7 / v1.36.11 とされています。修正版は「先に結論」の表のとおりです。

Wiz のまとめでは、ほかに JavaScript ランタイムの Bun、サービスメッシュの Istio、Go で書かれた Brotli 実装の andybalholm/brotli も挙げられています。各プロジェクトの告知では、次のとおりです。

深刻度は出典によって異なります。

出典 評価
Node.js のアドバイザリ High
NVD Critical(CVSS 3.1 で 9.8)
GitHub Advisory Database(GHSA-9v8h-qqgm-4m35) Critical(CVSS 4.0 で 9.2)
Envoy のアドバイザリ Critical(CVSS 4.0 で 9.2)
Ubuntu 優先度 high(理由として CISA-KEV を記載)

悪用については、Imperva の分析が修正前の公式 Node.js 24.22.0(Linux arm64)のイメージでコード実行を実証しています。Imperva は、攻撃が毎回成功するわけではないこと、すべてのビルドやアプリで試したわけではないことも書いています。2026年11月2日時点で、GitHub には10月22日に作られた、検証コードを公開するとうたうリポジトリもあります(中身は確かめていません)。

10月27日には、CISA が KEV(Known Exploited Vulnerabilities Catalog)にこの脆弱性を追加しました。KEV は、実際に悪用されたことが確認された脆弱性を CISA がまとめたカタログです。米国の連邦政府機関向けの対応期限は11月10日とされています。CISA は、確認された攻撃の内容を公開していません。一方、Microsoft のセキュリティ更新プログラム ガイドは、10月31日の改訂の時点で「Exploited: No」としています。悪用の評価は出典によって分かれています。

攻撃の流れ

攻撃者が用意する必要があるのは、主に「アプリが解凍する Brotli データの中身」です。GitHub Advisory Database(GHSA-9v8h-qqgm-4m35)の CVSS ベクトルでも、必要な権限はなし(PR:N)、ユーザーの操作も不要(UI:N)とされています。一方で、攻撃の成立には条件がある(AT:P)とも評価されています。Imperva が検証した経路は、受信した要求ボディを解凍する経路と、外部から取得した応答を解凍する経路の2つです。次の図は、Imperva の分析をもとに自作しました。

  1. 前提条件: 修正前の Node.js 22 / 24 / 26 で動くアプリが、攻撃者が中身を決められる Brotli データを解凍することです。
  2. 侵入・悪用: 要求ボディの経路ではボディパーサーに、応答の経路では fetch() に、Brotli データを解凍させます。
  3. 影響: クラッシュやプロセスのメモリの漏えい、条件によっては Node.js のプロセスの権限でのコード実行につながります。

コードに書かなくても解凍が起きる経路

zlib.brotliDecompress() を自分で呼んでいなくても影響を受けます。

  • body-parser の README には、すべてのパーサーが gzip、br(Brotli)、deflate、zstd を自動で解凍すると書かれています。圧縮ボディを解凍するかを決める inflate オプションのデフォルト値は true です。
  • Imperva は json() と raw() を設定した経路を検証しています。対象は Express 5.0.0(body-parser 2.0.1)、Express 5.3.0(body-parser 2.3.0)、body-parser 2.0.0 単体です。その間のバージョンは個別には試していないと書かれています。
  • body-parser 2.4.0(10月22日公開)は Imperva の検証対象に入っていません。2.4.0 で zstd の自動解凍が加わりましたが、br を自動で解凍する点と inflate のデフォルト値は変わっていません。
  • Express 4 が使う body-parser 1.x の README では、自動で解凍するのは gzip と deflate とされていて、br は含まれていません。
  • Imperva が検証した経路のとおり、Node.js の組み込みの fetch() は、Content-Encoding: br の応答を自動で解凍します。
  • 一方、Imperva によると、Node.js の素の HTTP サーバーは、要求を受け取るだけでは Brotli のボディを解凍しません。

Python でも同じ構図です。Aikido によると、urllib3 と requests は、Brotli か brotlicffi が入っていれば Brotli の応答を解凍します。httpx は httpx[brotli] を入れた場合、aiohttp は Brotli が入っている場合に影響を受けます。

Aikido は、応答の経路が成立する機能の例として、Webhook の配信、URL のプレビュー、クローラーを挙げています。Aikido の解説は Python を中心にしたもので、その後、ほかの言語にも範囲を広げています。

なぜ成立するのか

デコーダーが「形式エラー」を「入力待ち」と判定した

Node.js のアドバイザリによると、不正なブロック切り替えが、恒久的な形式エラーではなく、入力がもっと必要だという一時的な状態として扱われていました。ブロック切り替えは、Brotli の圧縮データの途中で、後続のデータを読むための符号表を切り替える指示です。

上流の修正コミット google/brotli の 0ed4268("Detect and report invalid stream earlier.")の差分を見ると、修正前は、ブロック切り替えを読む関数 DecodeBlockTypeAndLength() が結果を真偽値で返していました。次の抜粋(Brotli 1.2.0 の修正前のコード)にある1つ目の return BROTLI_FALSE は、ブロックの種類が1つしかなく、切り替えが形式として成り立たない場合に返す値です。2つ目は、符号を読むための入力がまだ足りない場合に返す値です。どちらも同じ値なので、呼び出し側からは区別できません。

c/dec/decode.c
  if (max_block_type <= 1) {
    return BROTLI_FALSE;
  }
  /* 中略 */
    if (!SafeReadSymbol(type_tree, br, &block_type)) return BROTLI_FALSE;

入力が足りているとは限らないときは、safe が真の経路を通ります。この経路では、呼び出し側のマクロ BROTLI_SAFE が BROTLI_FALSE を一律に「入力待ち」(BROTLI_DECODER_NEEDS_MORE_INPUT)へ読み替えていました。次の抜粋も Brotli 1.2.0 の修正前のコードです。

c/dec/decode.c
#define BROTLI_SAFE(METHOD)                       \
  {                                               \
    if (safe) {                                   \
      if (!Safe##METHOD) {                        \
        result = BROTLI_DECODER_NEEDS_MORE_INPUT; \
        goto saveStateAndReturn;                  \
      }                                           \
    } else {                                      \
      METHOD;                                     \
    }                                             \
  }

CISA は KEV で、この脆弱性の名称を「Google Brotli Unchecked Return Value Vulnerability」とし、CWE-252(戻り値の未確認)と CWE-787(範囲外書き込み)を挙げています。

「入力待ち」が終わらず、8 バイトのバッファの外へ書き込む

ストリーミングで解凍するとき、デコーダーは入力の区切りをまたぐデータを、内部の 8 バイトのバッファ(s->buffer.u8)にためて読みます。このバッファを使って読んでいる途中で「入力待ち」になると、次の入力から 1 バイトをバッファに足して読み直します。次の抜粋も Brotli 1.2.0 の修正前のコードです。

c/dec/decode.c
          } else if (*available_in != 0) {
            /* Not enough data in buffer, but can take one more byte from
               input stream. */
            result = BROTLI_DECODER_SUCCESS;
            BROTLI_DCHECK(s->buffer_length < 8);
            s->buffer.u8[s->buffer_length] = **next_in;
            s->buffer_length++;

本当に入力が足りないだけなら、数バイト足せば読み終わり、バッファは空に戻ります。一方、形式エラーは何バイト足しても解消しません。そのため「入力待ち」が終わらず、バッファへの追記が 8 バイトを超えても続きます。

上限の確認は BROTLI_DCHECK しかありません。このマクロは、BROTLI_DEBUG か BROTLI_ENABLE_LOG を定義したときだけ assert になります(c/common/platform.h)。通常のビルドでは何もしないため、9 バイト目以降の書き込みが、バッファの直後にあるデコーダーの状態を上書きします。

Node.js のアドバイザリが説明している 8 バイトの内部バッファへの追記は、この処理に当たると読めます。NVD の説明でも、影響として隣接するデコーダーの状態のメモリの破壊(corrupt adjacent decoder state memory)が挙げられています。

次の図は、修正前のコードをもとに自作しました。

修正後は形式エラーとして処理を止める

修正後の DecodeBlockTypeAndLength() は、真偽値ではなくエラーコードを返します。次の抜粋は、修正コミットの差分から取りました。

c/dec/decode.c
  if (max_block_type <= 1) {
    return BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH;
  }
  /* 中略 */
    if (!SafeReadSymbol(type_tree, br, &block_type)) {
      return BROTLI_DECODER_NEEDS_MORE_INPUT;
    }

切り替えが形式として成り立たない場合は、新しいエラーコード BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH(-17)を返します。入力が足りない場合は BROTLI_DECODER_NEEDS_MORE_INPUT を返します。呼び出し側では、新しいマクロ BROTLI_SAFE_WITH_STATUS が受け取った値をそのまま結果にして処理を止めます。Imperva も、上流の修正では不正な切り替えに対して形式エラーを返し、呼び出し元へ伝えるようになったと説明しています。Node.js は、この -17 を zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH として公開しました。

Brotli は npm の依存関係ではなくランタイム側にある

Node.js は Brotli をランタイムに組み込んでいます。ランタイムは npm のパッケージではないため、この問題は package-lock.json の依存関係には現れません。古い Node.js を含むコンテナイメージや単一実行ファイルは、ランタイムを入れ替えて作り直すまで影響を受けたままです。

ただし、Ubuntu や Debian の公式リポジトリから入れた nodejs パッケージは、同梱の Brotli ではなく、システムの brotli ライブラリを使います(確かめ方は後述)。

自分の環境が影響を受けるか確かめる

Node.js が修正版かを確かめる

node -v

冒頭の「先に結論」の表に挙げた修正版より古ければ対象です。ただし、ディストリビューション公式の nodejs パッケージと AWS Lambda は、後述の方法で確かめます。Imperva も、ホストとイメージごとに確かめるよう勧めています。コンテナの場合は、イメージの中のランタイムのバージョンを確かめます。<image> には確かめたいイメージ名を入れます。

docker run --rm --entrypoint node <image> -v

--entrypoint node を付けているのは、ENTRYPOINT でアプリの起動コマンドを定義したイメージでも、node -v を実行できるようにするためです(docker run のリファレンス)。公式イメージの node:22.23.4-alpine で試すと、v22.23.4 と表示されました。

process.versions.brotli ではなく定数の有無で見分ける

判定は Node.js 本体のバージョンで行い、修正が入ったかの手がかりには zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH の有無を使います。process.versions の brotli の値は修正の前後で変わらないため、判定には使えません。

値が変わらないのは、修正の入れ方によるものです。22.23.4 / 24.22.1 / 26.12.1 のリリースノートを見ると、CVE-2026-111497 の修正は Brotli 本体のバージョン更新ではなく、上流の修正コミットの取り込みです(リリースノートでの記載は deps: brotli: cherry-pick 0ed4268a4401abcb44720963)。あわせて、修正後のエラーコード -17 を zlib.constants に公開する変更も入っています。

この点は Node.js の issue(nodejs/node#67075)でも質問されています。修正を担当した Antoine du Hamel 氏は、Brotli 1.2.1 が出る前にセキュリティリリースを出したため、個別のコミットを取り込んだと説明しています。そのうえで、バージョンの文字列で判定するスキャナーは、代わりに zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH があるかを確かめるべきだと回答しています。

手元でも、修正前の 24.22.0(Imperva が検証したバージョン)と修正版の 24.22.1 を比べました。Ubuntu 24.04(arm64)のコンテナ上で、nodejs.org から取得した公式の Linux arm64 バイナリを使い、次の値を表示するだけのスクリプトを実行しました。

check.js
const zlib = require("node:zlib");
console.log(
  process.version,
  "brotli=" + process.versions.brotli,
  "BLOCK_SWITCH=" + zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH,
);

修正前の Node.js 24.22.0 と修正版の 24.22.1 で、process.versions.brotli と zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH を表示した結果。どちらも brotli=1.2.0 で、定数は 24.22.0 が undefined、24.22.1 が -17

どちらも brotli=1.2.0 で、違いは定数の有無だけでした。mise で入れた 22.23.1 と 22.23.4(macOS arm64)でも、同じスクリプトで次の結果になりました。

v22.23.1 brotli=1.1.0 BLOCK_SWITCH=undefined
v22.23.4 brotli=1.1.0 BLOCK_SWITCH=-17

26.x と 24.22.2 は、nodejs.org から取得した公式の macOS arm64 バイナリで試しました。修正前の 26.12.0 だけ定数がなく、修正版の 26.12.1 と、その後の 26.13.0 / 24.22.2 には定数がありました。

v26.12.0 brotli=1.2.0 BLOCK_SWITCH=undefined
v26.12.1 brotli=1.2.0 BLOCK_SWITCH=-17
v26.13.0 brotli=1.2.0 BLOCK_SWITCH=-17
v24.22.2 brotli=1.2.0 BLOCK_SWITCH=-17

process.versions.brotli の値を GHSA の対象範囲(1.0.3〜1.2.0)と照らし合わせると、22.x の 1.1.0 も 24.x / 26.x の 1.2.0 も範囲内に入ります。そのため、バージョンの文字列で判定すると、修正済みの 22.23.4 や 24.22.1 も対象と誤判定します。

確かめたのは、nodejs.org の公式バイナリ(Linux arm64 と macOS arm64)、mise で入れた 22.x(macOS arm64)、node:22.23.4-alpine と node:24.22.2-slim(arm64)のイメージだけです。どちらのイメージも定数は -17 でした。Windows や x64 のビルドで同じ定数が追加されているかは確かめていないため、これらのビルドで定数を手がかりにする場合は、自分の環境で修正前後の値を比べる必要がありそうです。ディストリビューション公式の nodejs パッケージでは、この見分け方は使えません(次の節)。

Ubuntu や Debian の nodejs パッケージは brotli のパッケージで確かめる

Ubuntu や Debian の公式リポジトリから入れた nodejs パッケージは、同梱の Brotli ではなく、システムの brotli ライブラリを使います。Ubuntu の CVE ページでは、jammy(22.04)以降の nodejs パッケージが「uses system brotli」として対象外になっていて、修正は brotli のパッケージ側で行われています。focal(20.04)の nodejs は同梱の Brotli を使い、2026年11月2日時点で修正は needed のままです。Debian のセキュリティトラッカーにも「nodejs and python-brotlicffi use system provided brotli library」と書かれています。公式の node Docker イメージや、ディストリビューション以外のリポジトリから入れた Node.js は、これらのトラッカーの対象ではありません。

どちらに当たるかは、node のバイナリがシステムの libbrotlidec をリンクしているかで見分けられます。リンクしていれば、brotli のパッケージ(libbrotli1)のバージョンを、各ディストリビューションのページの修正版と比べます。

ldd "$(command -v node)" | grep -i brotli
dpkg-query -W libbrotli1

Ubuntu 24.04(arm64)のコンテナに公式リポジトリの nodejs を入れ、この2つのコマンドと check.js を実行すると、次のようになりました。

$ ldd "$(command -v node)" | grep -i brotli
	libbrotlidec.so.1 => /lib/aarch64-linux-gnu/libbrotlidec.so.1 (0x0000e1a5cc930000)
	libbrotlienc.so.1 => /lib/aarch64-linux-gnu/libbrotlienc.so.1 (0x0000e1a5cc880000)
	libbrotlicommon.so.1 => /lib/aarch64-linux-gnu/libbrotlicommon.so.1 (0x0000e1a5cb940000)
$ dpkg-query -W libbrotli1
libbrotli1:arm64	1.1.0-2ubuntu0.1
$ node check.js
v18.19.1 brotli=1.1.0 BLOCK_SWITCH=undefined

libbrotli1 の 1.1.0-2ubuntu0.1 は、Ubuntu の CVE ページで noble(24.04)の修正版とされているバージョンです。一方で、定数は undefined でした。この nodejs(v18.19.1)は定数を公開する変更を含まないため、修正済みの libbrotli を使っていても定数は出てきません。定数は、Node.js のソース(src/node_zlib.cc)が brotli のヘッダーの定義を公開しているもので、値は Node.js をビルドしたときに決まります。システムの brotli を使うビルドでは、定数の有無から、実行時に読み込むライブラリが修正済みかどうかは分かりません。

比較のため、公式の node:24.22.2-slim イメージで同じ ldd を実行すると、libbrotli は表示されませんでした。このイメージの Node.js は Brotli をバイナリに同梱していて、定数は -17 でした。

AWS Lambda はランタイムバージョンで確かめる

AWS のセキュリティ情報(2026-155-AWS)によると、Lambda の Node.js と .NET のランタイムが影響を受けます(.NET はこの記事では扱いません)。Node.js 22 / 24 / 26 の ZIP の関数は、セキュリティ修正の Phase 1 / Phase 2 が完了済みで、コンテナイメージのベースイメージも更新済みとされています。Node.js 18 / 20 などのサポート終了のランタイムには修正が出ません。

Node.js 24 のランタイムについては、24.22.1 に既知のリグレッションがあったため、AWS は当初、それまでの安定版に修正だけを適用しました。そのため、修正済みでも process.version は変わりません。AWS はその後、リグレッションが直った 24.22.2 への移行を進めています。10月30日の更新の時点では、この移行の Phase 2(Lambda が自動でランタイムを更新する段階)は10月31日に終わる予定とされていて、11月2日時点で完了の追記は確認できていません。

修正済みかどうかは、関数のログの INIT_START の行に出るランタイムバージョンで判断します。この行は、実行環境のコールドスタートごとに出力されます。AWS が示す基準は、Node.js 22 が nodejs:22.mainline.v157 以上、Node.js 24 が nodejs:24.mainline.v114 以上です。Node.js 26 の基準値は示されていません。

Python の Brotli のバージョンを確かめる

Aikido は次のコマンドを挙げています。pipdeptree は依存関係をツリーで表示するツールで(別途インストールが必要)、-r -p Brotli を付けると、Brotli に依存しているパッケージを逆にたどれます。

pip show Brotli brotlicffi
pipdeptree -r -p Brotli

pipdeptree は手元では試していません(pip show は次のコンテナの例で試しました)。

コンテナでは、イメージの中で pip show を実行します。Aikido は docker run --rm <image> pip show Brotli brotlicffi を挙げていますが、ENTRYPOINT でアプリの起動コマンドを定義したイメージでは、pip は実行されません。Node.js の場合と同じく、--entrypoint で pip を指定します。

docker run --rm --entrypoint pip <image> show Brotli brotlicffi

python:3.13-slim に Brotli 1.2.1 を入れ、ENTRYPOINT でアプリを起動するテスト用のイメージで試しました。--entrypoint を付けないと、アプリが起動して app started と表示されるだけでした。付けると、次のように表示されました(4行目以降は省略)。

WARNING: Package(s) not found: brotlicffi
Name: brotli
Version: 1.2.1

「先に結論」の表に挙げた範囲なら対象です。

AWS によると、Lambda の Python のランタイム自体は Brotli を含みません。関数のデプロイパッケージに Brotli を入れている場合は、修正版で再デプロイします。

優先して更新する経路

次のどちらかに当てはまるサービスから先に更新します。この記事の暫定策は受信した要求ボディの経路しか守れないため、1つ目を最優先にします。

  1. 外部のサーバーへリクエストを送り、その応答を解凍する機能(Webhook、URL のプレビュー、クローラー、外部 API の取得など)
  2. 圧縮された要求ボディを受け付ける API(Express 5 / body-parser 2.x のパーサーをデフォルトの設定で使う場合を含む)

守り方

修正版へ更新する

  • Node.js を各系列の修正版以上へ更新します。20.x 以前はサポート中の系列へ移ります。
  • ベースイメージ、CI のランナー、単一実行ファイルなど、Node.js のバイナリを同梱する成果物を作り直します。入れ替えたバイナリを node -v で確かめてから、プロセスを再起動します。
  • ディストリビューション公式の nodejs パッケージを使っている場合は、brotli のパッケージ(libbrotli1)を更新し、Node.js のプロセスを再起動します。
  • AWS Lambda は、次の「AWS Lambda に修正を適用する」の節のとおりに適用します。
  • Python は Brotli / brotlicffi を更新し、lockfile とイメージを作り直します。
  • Envoy は「先に結論」の表の修正版以上へ、Bun、Istio、Go の andybalholm/brotli は「何が起きたのか」で挙げた修正版以上へ更新します。

マネージドな実行環境では、プラットフォーム側の対応も確かめます。たとえば Vercel の告知(10月29日付)によると、太平洋時間の10月28日午前11時以降、Vercel の新しいデプロイはすべて修正済みの Node.js を使います。Node.js 20.x のプロジェクトは 22.x か 24.x へ変えて再デプロイする必要がある、とも書かれています。

あわせて Vercel は、修正版の展開が終わるまでの措置をプラットフォーム側で取っています。修正前の Node.js を使う新しいデプロイでは、fetch() と node:http / node:https の外向きのリクエストで Brotli の応答を解凍しません。既存のプロジェクトには、再デプロイすると適用されます。どういう条件で修正前の Node.js が新しいデプロイに使われるのかは、告知には書かれていませんでした。「更新までの暫定策」の節で紹介する暫定策は、これとは別に自分で設定するものです。

AWS Lambda に修正を適用する

コンテナイメージの関数は、更新されたベースイメージから作り直して再デプロイします。ZIP の関数への適用のされ方は、ランタイムの更新モードで変わります(Lambda のドキュメント)。

  • Auto(デフォルト): Lambda が自動で更新します(Phase 2)。それより前でも、関数のコードか設定を更新すると適用されます(Phase 1)。
  • Function update: 関数のコードか設定を更新したときだけ適用されます。
  • Manual: 指定したランタイムバージョンを使い続けるため、更新されません。AWS は Manual を、ランタイムの更新で問題が起きたときに切り戻すための一時的なモードと位置付けていて、できるだけ早く Auto か Function update に戻すよう勧めています(責任共有モデルのページ)。

Function update の関数や、Phase 2 を待たずに適用したい関数は、関数を実際に変更する必要があります。AWS は「A zero-change update is not sufficient.」と書いており、環境変数 TRIGGER_RUNTIME_UPDATE を足して設定を更新するスクリプトを載せています。CDK などで管理している関数は、IaC 側で設定を変えてデプロイします。CLI で環境変数を足すと、コードの定義と実際の設定に差分が生じるためです。

なお、Node.js 24 のランタイムが 24.22.2 に移ったあと、@h4ad/serverless-adapter を使う関数で不具合が報告されています(H4ad/serverless-adapter#423)。ストリーミングの応答が終わらず、タイムアウトするというものです。issue によると、24.22.2 はランタイムバージョン nodejs:24.mainline.v115 で、v114 は Node.js 24.21.0 です。原因は 24.22.0 で入った Node.js の http の変更とされ、アダプター側は v4.4.1 で修正されています。ランタイムが変わったあとは、ストリーミングの応答が最後まで返るかも確かめます。

更新までの暫定策

圧縮された要求ボディを受け付ける必要がない経路では、body-parser の inflate を false にすると、圧縮ボディを拒否できます(body-parser の README)。inflate はパーサーごとのオプションなので、raw()、text()、urlencoded() も使っている場合はそれぞれに指定します。

app.js
app.use(express.json({ inflate: false }));

拒否の対象は Content-Encoding が identity(圧縮なし)以外のすべての要求で、br に限らず gzip、deflate、zstd も 415(encoding.unsupported)になります(README の Errors 節)。設定する前に、WAF やアプリのログで、identity 以外の Content-Encoding を付けた要求があるかを確かめます。

エッジや WAF で Content-Encoding: br の要求を拒否する方法もあります。AWS のセキュリティ情報には AWS WAF のサンプルルール Node2Shell_BROTLI が載っていて、まず Count(一致した要求を記録するだけで止めない)で動かしてから Block(遮断する)に切り替える手順になっています。AWS は、このルールが Brotli で圧縮した正規のアップロードも止めること、すべての Brotli の要求に一致するとは限らない例であることも書いています。Cloudflare は10月25日、Cloudflare Managed Ruleset に「Brotli - Request Content-Encoding - CVE:CVE-2026-111497」を追加しました。追加時のアクションは Log で、そのままでは要求を止めません。Vercel と Imperva も WAF のルールを追加済みです。

ただし、inflate の無効化も WAF での拒否も、守れるのは受信した要求ボディの経路だけです。AWS も「AWS WAF inspects inbound requests only.」と注記しています。Vercel の告知でも、WAF の対象は Vercel を通る通信だけで、関数が外部から取得する応答は対象外とされています。2026年11月2日時点で確認した一次情報には、自分のアプリで設定できる、fetch() の応答の経路を守る暫定策は示されていませんでした(Vercel のようにプラットフォーム側で解凍を止める措置は、「修正版へ更新する」の節のとおりです)。この経路は Node.js を更新して守ります。

検知の手がかり

解凍中に起きたプロセスの異常終了やデコーダーのエラーは、優先して調べます。Imperva は、どちらも単独では悪用の確かな指標にならないとしたうえで、ランタイムのバージョン、アプリのログ、圧縮データの送り元と突き合わせるよう書いています。

まとめ

  • CVE-2026-111497 は、Node.js に同梱された Brotli のデコーダーの問題です。不正なブロック切り替えと入力不足が同じ戻り値になるため、形式エラーが「入力待ち」と誤判定され、8 バイトの内部バッファの外への書き込みが続きます。修正後は専用のエラーコード(-17)を返して処理を止めます。Node.js 22.23.4 / 24.22.1 / 26.12.1、Python の Brotli 1.2.1 / brotlicffi 1.2.1.0、Envoy の各修正版で直っています。
  • 10月27日には CISA の KEV に追加されました。GitHub には、検証コードを公開するとうたうリポジトリもあります。
  • 解凍はアプリのコードの外で起きます。fetch() は応答を、Express 5 / body-parser 2.x のパーサー(express.json() など)は要求ボディをデフォルトで解凍するため、Brotli を使っているつもりがなくても対象になります。
  • Node.js は、ホストとイメージのそれぞれで node -v を実行して確かめます。process.versions.brotli は修正の前後で変わらないため使えず、修正が入ったかの手がかりは zlib.constants.BROTLI_DECODER_ERROR_FORMAT_BLOCK_SWITCH の有無です。ディストリビューション公式の nodejs パッケージは brotli のパッケージ、Lambda は INIT_START のランタイムバージョン、Python は pip show で確かめます。
  • この記事の暫定策で守れるのは要求ボディの経路だけなので、外部の URL を取得するサービスから先に更新します。

誰かの参考になれば幸いです。


参考リンク:

この記事をシェアする

関連記事