Node.js 脆弱性:Node2Shellのパッチと Node.js バージョンの確認(WordPress)

公正取引委員会の広告文 Ver 3.1
公正取引委員会の広告文 Ver 3.2(モバイル専用)
テスト環境 RHEL系AlmaLinux 9.8Nginx + Apache · WordPress 7.1.32026-10-23 確認

Node2Shellは、2026年10月21日にパッチが公開されたNode.jsの脆弱性CVE-2026-111497の別名で、公式のタイトルはGoogleのBrotliデコーダーの境界外書き込みの欠陥です。細工されたBrotli圧縮データを展開した瞬間にリモートコード実行につながるおそれがあるため、サーバーのNode.js・Bun・libbrotliのバージョンを確認し、パッチ適用バージョンに上げることが先決です。

WordPress 7.1.3が入ったNginx + Apache構成のサーバーで、Brotliデコーダーを使うパッケージと、WordPressのリモートリクエストがlibbrotliを経由する経路を確認した手順です。

1️⃣ Node.js 脆弱性 Node2Shell:欠陥の仕組みと影響範囲

Node2Shellという名前は、セキュリティ企業Impervaの研究チームが10月22日の研究記事で付けた別名です。nodejs.orgの告知のタイトルは[Brotli decoder out-of-bounds write permits remote code execution]です。欠陥はNode.jsが内蔵するGoogle Brotliデコーダーのソース(c/dec/decode.c)にあるため、アプリケーションのコードを直しても防げません。

NVDの記録によると、Google Brotli 1.0.3〜1.2.0のデコーダーは細工された圧縮ストリームを展開するときにバッファの終端を越えて書き込み、隣接するデコーダーの状態メモリを破壊します。ログインしていないリモートの攻撃者がこの欠陥を使って、サーバーのメモリの内容を読み出したり、サービスを停止させたりし、任意のコードを実行できます。

深刻度の表記は2つあります。nodejs.orgの告知はHighと評価し、CVEを発行したGoogleはCVSS 4.0で9.2(Critical)と評価しました。NVDは10月23日時点で独自の分析が終わっていないため、NVDのスコアはまだありません。弱点の種類は戻り値の未検証(CWE-252)と境界外書き込み(CWE-787)です。

nodejs.orgのセキュリティ告知によると、Node.js 22.x・24.x・26.xのサポート中の系列はすべて影響を受け、パッチ適用バージョンは22.23.4・24.22.1・26.12.1です。サポートが終了した系列は、セキュリティ告知が出るたびに常に影響を受けるものとして扱うと同じ告知が明らかにしています。

同じデコーダーを使うプログラムは、Node.js以外でもすべて対象です。Googleは10月21日に、同じCVEを修正したBrotliライブラリの1.0.10・1.1.1・1.2.1の3つのバージョンを出しました。Bunは10月22日の1.4.4の告知でセキュリティ修正の一覧にBrotliを挙げ、リポジトリのissueは、そのバージョンにbrotliを更新した修正が含まれていると明らかにしています。

コピー先影響範囲パッチ適用バージョン根拠
Node.js22.x・24.x・26.xのサポート中の系列すべて・サポート終了の系列22.23.4 · 24.22.1 · 26.12.1nodejs.orgのセキュリティ告知(2026-10-21)
Google Brotliライブラリ1.0.3 ~ 1.2.01.0.10 · 1.1.1 · 1.2.1NVDの記録・google/brotliのリリース(2026-10-21)
Bun1.4.4の告知のセキュリティ修正一覧にBrotli1.4.4Bunの告知(2026-10-22)
ディストリビューションのlibbrotliパッケージBrotli 1.0.3〜1.2.0をもとに作られたパッケージディストリビューションのセキュリティ告知が出すバージョン(2026-10-23時点ではまだなし)各ディストリビューションのセキュリティトラッカーのページ

Node.js自体は、受け取ったBrotliデータを自分で展開しません。ホスティング会社Vercelのセキュリティ告知によると、影響を受けるのはアプリやライブラリが外から来た圧縮データを展開する箇所です。組み込みのfetch()がbrの応答を受け取るときや、Express 5・body-parser 2がbrのリクエスト本文を展開するときがその例で、受け取るだけの標準のHTTPサーバーは展開しません。

実際の悪用は10月23日時点で確認されていません。CISAの既知の悪用された脆弱性カタログ(KEV、10月21日版)にこのCVEはありません。CVEの記録でCISAが付ける悪用の段階は、10月23日に[概念実証(PoC)公開]に変わりました。

デコーダーがどこに入っているかで、やることが決まります。Node.jsの公式バイナリはデコーダーを実行ファイルの中に内蔵し、Debianのnodejsパッケージのようにシステムのlibbrotliを使うビルドもあります。Linuxディストリビューションは同じソースをlibbrotliパッケージとしてインストールし、curlのようなプログラムが共用します。

2️⃣ WordPress・PHPサイトも対象:リモートリクエストがlibbrotliで応答を展開する経路

Node.jsをインストールしていないWordPressサーバーにも、同じデコーダーがlibbrotliパッケージとして入っています。WordPress・curl・PHP側でこのCVEを扱った公式の言及は10月23日時点でなく、curlの公式の脆弱性一覧にもこのCVEはありません。そのためこの章は、コアのコードとcurlのドキュメント、テストサーバーでの実測で構造だけを確認したものです。

実測したNginx + Apache構成のサーバーで、デコーダーのライブラリ(libbrotlidec)を必要とするパッケージは5つで、そのうちWordPressとつながるのはlibcurlです。

rpm -q --whatrequires 'libbrotlidec.so.1()(64bit)' | sed 's/-[0-9].*//'
freetype
brotli-devel
libjxl
libcurl
libsoup

libcurlがlibbrotliを必要とする理由は、圧縮されたHTTP応答の展開にあります。curlの公式ドキュメントによると、brotliライブラリとともにビルドされたlibcurlは、Content-Encodingがbrの応答を自動で展開します。そのlibcurlをPHPのcurl拡張がそのまま呼び出します。

ldd /usr/lib64/libcurl.so.4 | grep brotli
    libbrotlidec.so.1 => /lib64/libbrotlidec.so.1 (0x00007f32803ed000)
    libbrotlicommon.so.1 => /lib64/libbrotlicommon.so.1 (0x00007f327f858000)
php -i | grep BROTLI
BROTLI => Yes

WordPressが使うlibcurlのバージョンは、管理画面の[ツール › サイトヘルス]の[情報]タブで[サーバー]セクションを開くと[cURL バージョン]の行に表示されます。brotliに対応しているかはその行に出ないため、上のようにサーバーのコマンドで確認します。

サイトヘルスの情報タブ画面:左メニューのツール › サイトヘルス、サーバーセクションのサーバー構造・Web サーバー・PHP バージョン・PHP SAPI・cURL バージョンの行 — Node.js 脆弱性Node2ShellのWordPress側の経路であるlibcurlのバージョンをcURL バージョンの行で確認(WordPress 7.1.3)

画像の[cURL バージョン]の行は、libcurlのバージョンと、一緒にビルドされたSSLライブラリを表示します。curlのドキュメントによるとbrは7.57.0から対応した方式なので、この値がそれより低ければbrの応答を展開できません。それ以上なら、ビルドにbrotliが入っているかをサーバーのコマンドで確認します。

WordPressコアのRequestsライブラリは、外部のURLにリクエストするとき、curlハンドルの[CURLOPT_ENCODING]の値を空の文字列に設定します。curlのドキュメントによると、空の文字列はそのlibcurlが対応するすべての圧縮方式を受け入れるという意味です。そのため相手のサーバーがbrで圧縮した応答を送ると、WordPressサーバーのlibbrotliがそれを展開することになります。

grep -n CURLOPT_ENCODING wp-includes/Requests/src/Transport/Curl.php
110:            curl_setopt($this->handle, CURLOPT_ENCODING, '');
212:            curl_setopt($this->handle, CURLOPT_ENCODING, 'none');

110行目がすべてのリクエストの既定値で、212行目は展開に失敗したリクエストを圧縮なしで送り直すときにだけ実行されます。外から直接突く方法はなく、コアやプラグインのアップデート確認、oEmbed、RSSフィードのようにWordPressが応答を受け取る相手のサーバーが先に乗っ取られている必要があります。

🧩 Node2Shellの攻撃の流れ
解説ダイアグラム
①外から来る圧縮データbrで圧縮されたリクエスト本文や相手のサーバーの応答
②アプリがBrotliデコーダーで展開する段階Node.js fetch() · Express 5 · Bun fetch() · WordPressはlibcurl → libbrotli
③Brotliデコーダーの欠陥細工されたストリームがバッファの終端を越えて書き込むCVE-2026-111497
④サーバーでコード実行メモリ漏えい · サービス停止 · サーバーの乗っ取り
✅ パッチ適用バージョンではこの流れが止まるNode.js 22.23.4 · 24.22.1 · 26.12.1Bun 1.4.4Brotli 1.0.10 · 1.1.1 · 1.2.1

流れは外から来たbr圧縮データで始まります。Node.jsのfetch()やExpress 5、Bunのfetch()、WordPressのlibcurlが呼び出したlibbrotliがそれを展開する段階で欠陥に触れます。その結果がサーバーでのコード実行につながります。パッチ適用バージョンはこの流れを止めるため、やることはデコーダーをパッチ適用バージョンに替えることです。

Apacheのmod_brotliは応答を圧縮するエンコーダーのライブラリ(libbrotlienc)だけをリンクするため、このモジュールで圧縮を有効にした設定はデコーダーの欠陥の経路に入りません。

ldd /usr/lib64/httpd/modules/mod_brotli.so | grep brotli
    libbrotlienc.so.1 => /lib64/libbrotlienc.so.1 (0x00007f34b4b79000)
    libbrotlicommon.so.1 => /lib64/libbrotlicommon.so.1 (0x00007f34b4a79000)

3️⃣ Node.js バージョンの確認:サーバーにNode.js・Bun・libbrotliがあるかとそのバージョン

まずはサーバーに何がインストールされているかを洗い出します。Node.jsは[node -v]でバージョンを見て、その値が22.23.4・24.22.1・26.12.1より低ければ影響を受けます。

自分の環境確認コマンド直る箇所
nodejs.orgのインストーラー・nvm・Docker公式イメージ[node -v]Node.js自体(22.23.4・24.22.1・26.12.1)
ディストリビューションのnodejs・lddにlibbrotliの行あり[ldd $(command -v node)]libbrotliパッケージ
ディストリビューションのnodejs・lddの結果0[ldd $(command -v node)]nodejsパッケージ(ディストリビューションのセキュリティ告知後)
共有ホスティングのNode.js Selector(CloudLinux alt-nodejs)事業者の告知・パッケージのrelease番号事業者が配布したパッケージ + アプリの再起動
Bun[bun --version]Bun 1.4.4以上
Node.jsなし(WordPressのみ)[ldd]でlibcurlを確認libbrotliパッケージ + php-fpmの再起動・それまでは暫定対策
マネージドホスティング事業者の告知・問い合わせ事業者が上げるパッケージ

PC(Windows・macOS)はnodejs.orgのパッチ適用バージョンのインストーラーで、nvmは[nvm ls]でバージョンを見たあと[nvm install 24]のように系列の番号で入れ直します。Docker公式のnodeイメージの22・24・26タグは10月22日にパッチ適用バージョンで作り直されたため、イメージを取得し直してビルドし、デプロイします。Vercelの告知は、自分でビルドしたコンテナイメージやCIの実行環境、ローカルの開発環境もパッチ適用バージョンに上げるよう勧めています。

dnf moduleのストリームで Node.jsをインストールした サーバーは、[node -v]にそのストリームの系列のバージョンが表示され、系列が同じでも末尾の番号がパッチ適用バージョンに届かなければ上げる必要があります。

node -v
v24.xx.x

Node.jsがシステムのlibbrotliを使うか内蔵のデコーダーを使うかは[ldd]で確認します。出力にlibbrotlidecの行があれば共用ライブラリを使うビルドで、下のように0なら内蔵のデコーダーなので、Node.js自体を上げないと直りません。

ldd $(command -v node) | grep -c brotli
0

Bunは[bun --version]でバージョンを確認し、1.4.4より低ければ上げます。

libbrotliパッケージは、RPM系では[rpm -q libbrotli]でバージョンを見ます。パッケージのバージョン番号はディストリビューションが付けたものなので、Googleのリリース番号とそのまま比べず、ディストリビューションのセキュリティ告知がこのCVE番号で出したバージョンと比べます。

rpm -q libbrotli
libbrotli-1.0.x-x.el9_x.x86_64

ディストリビューションの告知が出たかは[dnf updateinfo info --cve CVE-2026-111497]で確認します。出力が空なら、そのリポジトリにはまだこのCVE向けの告知がありません。

dnf updateinfo info --cve CVE-2026-111497

バージョン番号はそのままで修正だけを取り込んだパッケージは、変更履歴にCVE番号が残ります。そのため[rpm -q --changelog libbrotli | grep CVE-2026-111497]で確認し、行が出ればパッチ済みのパッケージです。

rpm -q --changelog libbrotli | grep CVE-2026-111497

10月23日時点で、Debian・Ubuntu・Red Hat・AlmaLinuxのlibbrotliパッケージには、このCVEのパッチ適用バージョンがまだありません。Alpineはnodejsパッケージだけパッチ適用バージョンが出ており、根拠は各ディストリビューションのセキュリティトラッカーのページです。そのため今は、ディストリビューションのセキュリティ告知が出るのを見守り、出たら上げるという順序になります。

Debian・Ubuntu系はパッケージ名がlibbrotli1で、あるマネージドホスティングのDebian 12のテストサーバーで同じ項目を確認しました。このサーバーでもlibcurlがlibbrotlidecをリンクし、PHPのcurl拡張がbrotliに対応していました。変更履歴は[zcat /usr/share/doc/libbrotli1/changelog.Debian.gz | grep CVE-2026-111497]で見ます。

dpkg -l libbrotli1 | tail -1
ii  libbrotli1:amd64 1.1.0-2+0~20240305.8+debian12~1.gbp62df71 amd64        library implementing brotli encoder and decoder (shared libraries)
ldd /usr/lib/x86_64-linux-gnu/libcurl.so.4 | grep brotli
    libbrotlidec.so.1 => /lib/x86_64-linux-gnu/libbrotlidec.so.1 (0x00007fa2baac6000)
    libbrotlicommon.so.1 => /lib/x86_64-linux-gnu/libbrotlicommon.so.1 (0x00007fa2b9cb9000)

4️⃣ Node.js のアップデート:パッチ適用バージョンに上げて再確認

マネージドホスティングや共有ホスティングではサーバーのパッケージを事業者が上げるため、Node.jsとlibbrotliのパッチ適用状況を事業者の告知や問い合わせで確認します。サーバーを自分で運用しているなら、Node.jsはnodejs.orgの告知のパッチ適用バージョンに、Bunは[bun upgrade]で1.4.4以上に上げます。

ディストリビューションのモジュールストリームでインストールしたNode.jsは、そのリポジトリにパッチ適用バージョンが上がってから[dnf upgrade nodejs]の形で上げます。nodejs.orgから直接ダウンロードしてインストールしたサーバーは、同じ場所のパッチ適用バージョンに入れ替えます。サポートが終了した系列はパッチ適用バージョンが出ないため、サポート中の22・24・26の系列に移ります。

共有ホスティングのNode.js Selector(CloudLinux alt-nodejs)はbrotliのコピーを別に抱えているため、システムのlibbrotliを上げても直りません。CloudLinuxの告知によると、事業者が配布したパッケージに替わったあとアプリを再起動して初めて新しいバイナリが実行されます。サポートが終了したバージョンは[node --version]の値が変わらないため、パッケージのrelease番号で確認すると同じ告知が明らかにしました。

libbrotliパッケージは、ディストリビューションのセキュリティ告知が出たらそのパッケージを上げ、ライブラリをメモリに読み込んでいるプロセスを再起動して初めて新しいライブラリに替わります。共有ライブラリはプロセスの起動時に読み込まれるため、WordPressサーバーではphp-fpmの再起動までがひとまとまりです。

上げたあとは、[node -v]がパッチ適用バージョン以上か、[rpm -q libbrotli]がディストリビューションの告知のバージョンかを見ます。変更履歴にCVE番号が入ったかは[rpm -q --changelog libbrotli | grep CVE-2026-111497]で確認します。php-fpmを再起動したら、サービスの状態とサイトの応答まで確認します。

systemctl is-active php-fpm
active
curl -s -o /dev/null -w '%{http_code}\n' 'https://example.com/?r=1'
200

5️⃣ パッチ前の暫定対策:WordPressのリモートリクエストでbrの展開をオフにする

Red HatのCVEの案内は、信頼できない場所から来たbr圧縮を受け入れないか、自動で展開しないようにアプリを設定するよう勧めています。

パッチ済みのパッケージが出る前でも、WordPress側の経路は今すぐ塞げます。WordPressはリモートリクエストのたびにcurlハンドルを渡す[http_api_curl]フックを用意しています。そこで[CURLOPT_ENCODING]をnullにすると、libcurlの自動展開がオフになります。

下のファイルを[wp-content/mu-plugins/]フォルダーに置くとすべてのリモートリクエストに適用され、ファイル名は例です。元に戻すときはこのファイルを削除します。

<?php
// no-brotli-decode.php — WordPress リモート リクエストで libcurl 自動 展開 オフ(libbrotli パッチ バージョンが 出る まで)
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}
add_action( 'http_api_curl', function ( $handle ) {
    curl_setopt( $handle, CURLOPT_ENCODING, null );
}, 99 );

[CURLOPT_ENCODING]を[gzip, deflate]に絞ればbrが外れると考えがちです。この値はリクエストヘッダーのAccept-Encodingからbrを外すだけなので、相手のサーバーがbrで応答するとlibcurlは受け取ったとおりに展開します。テストサイトで同じ値を使ってhttpbin.org/brotliを受け取った結果、Content-Encodingがbrの応答がそのまま展開されて読めました。

curlのドキュメントによると、自動展開をオフにする値はnullだけで、nullならAccept-Encodingヘッダーを送らず、受け取った本文も展開しません。gzip・deflateの応答はWordPressのRequestsライブラリが自分で展開してそのまま読め、brの応答だけが展開されないまま残ります。

このフックは、Requestsがcurlを実行する直前に呼ぶ[curl.before_send]の位置にWordPressがつないだものです。Curl.phpの174行目で呼び出し205行目で実行するため、コアが110行目で入れた空の文字列の値がその間に上書きされます。

適用されたかは下の確認用ファイルで確かめます。httpbin.orgは受け取ったリクエストヘッダーを返し、br・gzipで圧縮した応答も返すテスト用のアドレスなので、WordPressが何を送り何を展開するかが一度に見えます。

<?php
// br-check.php — WordPress リモート リクエストが どの 圧縮を 送り 展開するか 確認(httpbin.org テスト アドレス)
$h = json_decode( wp_remote_retrieve_body( wp_remote_get( 'https://httpbin.org/headers' ) ), true );
echo 'Accept-Encoding: ', isset( $h['headers']['Accept-Encoding'] ) ? $h['headers']['Accept-Encoding'] : '(none)', PHP_EOL;
foreach ( array( 'brotli', 'gzip' ) as $p ) {
    $r = wp_remote_get( 'https://httpbin.org/' . $p );
    $readable = strpos( wp_remote_retrieve_body( $r ), ': true' ) !== false;
    echo $p, ': Content-Encoding=', wp_remote_retrieve_header( $r, 'content-encoding' ), ' / ', strlen( wp_remote_retrieve_body( $r ) ), ' bytes / ', ( $readable ? 'readable' : 'not decoded' ), PHP_EOL;
}

ファイルを入れる前は、Accept-Encodingにbrが入り、brの応答が展開されて読めます。

wp eval-file br-check.php
Accept-Encoding: deflate, gzip, br
brotli: Content-Encoding=br / 316 bytes / readable
gzip: Content-Encoding=gzip / 317 bytes / readable

ファイルを入れたあとは、Accept-Encodingヘッダーがなくなり、brの応答は展開されないまま残り、gzipの応答はそのまま読めます。

wp eval-file br-check.php
Accept-Encoding: (none)
brotli: Content-Encoding=br / 199 bytes / not decoded
gzip: Content-Encoding=gzip / 272 bytes / readable

ディストリビューションのパッチ済みlibbrotliパッケージを上げ、php-fpmの再起動まで終わったら、このファイルを削除して元に戻します。

🔢 FAQ & おすすめコンテンツ

WordPressの自動更新が扱うのはコアとプラグイン、テーマ、翻訳だけで、libbrotliとNode.jsはサーバーにインストールされたパッケージなので、サーバー管理者やホスティング会社が上げます。上げたあとphp-fpmの再起動まで終わって初めて、WordPressのリモートリクエストが新しいライブラリで処理されます。

プラグインがWordPress HTTP APIを通っても、curl関数を直接呼び出しても、同じlibcurlを通ります。直すのはサーバーのlibbrotliパッケージで、パッチ適用バージョンが出る前は5️⃣の設定でWordPressのリモートリクエストの自動展開をオフにできます。curl関数を直接呼び出しながら[CURLOPT_ENCODING]を設定していないコードは、curlのドキュメントによると圧縮された応答を自動で展開しません。直接呼び出しながら空の文字列に設定したコードは[http_api_curl]フックの外なので、そのプラグインのリクエストだけ別に確認します。

NVDとcve.orgのCVE-2026-111497の記録(CNA Google)に、欠陥の位置(c/dec/decode.c)と影響を受けるバージョン、弱点の種類があります。google/brotliのリリースv1.2.1・v1.1.1・v1.0.10のノートには修正した事実が書かれています。攻撃の再現の詳細はこの記事に入れていません。

ℹ️ 提携について
当サイトのコンテンツにはアフィリエイトリンクが含まれています。訪問者がこのリンクを通じて商品やサービスを購入すると、当サイトは販売元から手数料を受け取ります。この過程において、購入者が支払う金額(イベント割引適用で金額が下がります↓)は上昇しません。掲載されている価格・割引・在庫情報は作成時点のものであり、実際とは異なる場合がありますので、ご購入前に販売元にて最終確認をお願いいたします。商品の選定および評価は独自の基準に基づいて行われており、手数料の支払いの有無は紹介順や評価内容に影響を与えません。

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

人間であることを証明してください: 4   +   9   =