Node.js の定例セキュリティリリースで、High 4 件を含む 10 件の脆弱性が修正された。本資料は、どの使い方のときに影響を受けるか(Web サーバ/HTTP クライアント/CLI)と、サポートが終了した Node.js 20 を使い続けている環境での判断材料をまとめる。
| 系列 | 状態 | サポート終了 | 修正版 |
|---|---|---|---|
| 26.x | Current | 2029-04-30 | 修正 v26.12.1 |
| 24.x | Active LTS | 2028-04-30 | 修正 v24.22.1 |
| 22.x | Maintenance LTS | 2027-04-30 | 修正 v22.23.4 |
| 20.x | End-of-Life | 2026-04-30 | 修正なし |
同梱の依存物も更新された。c-ares・Undici(全系列)、OpenSSL(24.x・22.x)、SQLite(22.x)、CA 証明書(侵害された eMudhra 中間 CA の除去。全系列)。
公式発表には回避策の記載が無く、対処は修正版への更新のみとされている。
| CVE | 深刻度 | 対象系列 | 内容 | 影響する側 |
|---|---|---|---|---|
| CVE-2026-111497 | High | 26 / 24 / 22 | Brotli デコーダの境界外書き込み。リモートコード実行のおそれ | 信頼できない Brotli データを展開する側 |
| CVE-2026-105483 | High | 26 / 24 | 全角数字で書いた IP で TLS のホスト名検証を回避(回帰バグ) | TLS クライアント |
| CVE-2026-106069 | High | 26 / 24 / 22 | 正規化で IP に化けるホスト名で TLS のホスト名検証を回避 | TLS クライアント |
| CVE-2026-105482 | High | 26 / 24 / 22 | dns.resolve* が NUL 文字以降を切り捨てる | 外部入力を名前解決する側 |
| CVE-2026-105481 | Medium | 26 / 24 / 22 | keep-alive のソケットが、要求を送る前に届いた応答を処理する | HTTP クライアント |
| CVE-2026-106072 | Medium | 26 / 24 / 22 | 非正規の IPv4 表記で、証明書の DNS 名が IP の接続先を認証する | TLS クライアント |
| CVE-2026-106074 | Medium | 26 / 24 / 22 | randomUUID()・randomInt() の乱数キャッシュが snapshot・SEA に焼き込まれ、全プロセスで同じ値になる | snapshot・SEA で配る CLI / サーバ |
| CVE-2026-106070 | Low | 26 | ZipFile がヘッダ検証失敗後に攻撃者の制御するバイト列を返す | ZIP を読む側 |
| CVE-2026-106071 | Low | 26 | Permission Model のワイルドカードで、ファイルの許可が広がりすぎる | --permission 利用時 |
| CVE-2026-106073 | Low | 26 / 24 / 22 | fs.realpathSync で Permission Model を回避 | --permission 利用時 |
不正な Brotli データの展開中に境界外書き込みが起きる。修正は Google の Brotli 本家のコミットの取り込みで、ブロック種別が 1 つしか宣言されていないストリームにブロック切り替えが現れたら即座に失敗させる。到達する API は zlib.brotliDecompress()・brotliDecompressSync()・createBrotliDecompress() と、応答の Content-Encoding: br を自動で展開する fetch()(Undici)。
Accept-Encoding: identity を送っても、悪意あるサーバが br で返せばデコーダが動く。Debian・Ubuntu のパッケージで入れた Node.js はシステムの libbrotli を使うことがあり、その場合の修正は libbrotli パッケージ側から来る。
この脆弱性は、報道や各社のブログで node2shell (Node2Shell) と呼ばれ、その後 HTTP2Shell と呼び直された。どちらも CVE-2026-111497 の 1 件を指す別名で、別の脆弱性ではない。欠陥は Node.js 固有ではなく、Google の Brotli ライブラリ (1.0.3〜1.2.0) そのものにある。
| 日付 (2026年) | 出来事 |
|---|---|
| 2月 | OpenAI が欠陥を発見。Brotli 本家のソースでは直ったが、セキュリティの修正と認識されず、修正を含むリリースが出ないままだった |
| 10月21日 | Node.js が修正版を公開。node2shell と呼ばれ始める |
| 10月22日 | Python の Brotli パッケージも影響を受けると公表 (Python2Shell とも) |
| 10月23日 | HTTP の圧縮を扱う多くのライブラリ・製品に及ぶとして HTTP2Shell と呼び直される。OpenAI も詳細を公開 |
深刻度は、CVE を採番した Google が CVSS 4.0 で 9.2 (Critical)、Node.js は High と評価している。Imperva は、改変していない公式の Node.js 24.22.0 のイメージで、任意のコードの実行を再現した (毎回は成功しない)。10月23日時点で、実際の攻撃は報告されていない。
json()・raw() 等。要求本文の Content-Encoding: br を展開するため、認証の前に、外部から 1 回の要求で届くfetch()。悪意あるサーバ、または改ざんされた応答の br を自動で展開するhttp.createServer。受け取るだけでは要求本文を展開しない| 対象 | 修正版 |
|---|---|
| libbrotli (C) | 1.2.1 / 1.1.1 / 1.0.10 |
| Bun | 1.4.4 |
| Python (Brotli・brotlicffi) | 1.2.1・1.2.1.0 |
| Envoy | 1.40.0・1.39.3・1.38.6・1.37.8 |
| Istio | 1.29.9・1.30.6・1.31.2 |
| curl・libcurl | 依存の libbrotli を更新。同梱しているアプリは作り直しが必要 |
.NET・PHP・Ruby・DuckDB・ClickHouse・7-Zip などにも Brotli を同梱する物があり、修正の状況は製品ごとに異なる。Apache・nginx は既定では要求本文の Brotli を展開しない。
tls.checkServerIdentity() が、IP の形をした接続先を、証明書の IP Address: ではなく DNS: 項目と照合してしまう。例: 0127.0.0.1 は url.domainToASCII() では 8 進として 87.0.0.1 と解釈されるが、OS は 127.0.0.1 へ接続する。DNS:87.0.0.1 の証明書で、別のマシンが本物として通ってしまう。
悪用には、クライアントが信頼する CA から IP の形をした DNS: 項目を持つ証明書を得る必要があり、条件は厳しい。接続先のホスト名を外部入力で決めるクライアントで問題になる。
c-ares が NUL を文字列の終わりとして扱う。'example.com\0.trusted.corp' を検査では .trusted.corp と見て許可しても、実際に問い合わせるのは example.com になる。dns.lookup()(OS のリゾルバ)は対象外で、dns.resolve*()・dns.reverse() が対象。
今回の High は、多くが Node.js が外へ通信するとき(クライアント側)の問題である。「Web サーバか CLI か」より、信頼できない相手のデータを受け取るかで判断する。
| 使い方 | 影響する CVE | 危険度の目安 |
|---|---|---|
Web サーバで、要求本文の Content-Encoding: br を展開する (Express 5・body-parser 2 の本文パーサは既定で展開する) | 111497 | 高 外部から未認証で到達しうる |
Web サーバが外部 API・利用者指定の URL へ fetch する(Webhook・プロキシ・OGP 取得・SSRF になりうる処理) | 111497・105481・TLS 3 件 | 高 接続先を利用者が選べるなら特に |
| Web サーバが静的配信・API 応答だけ(圧縮は送るだけ、外へ通信しない) | ほぼ無し | 低 圧縮して送る側は対象外 |
| CLI がインターネットからダウンロードする(npm 系ツール・更新確認・スクレイピング) | 111497・TLS 3 件 | 中 接続先が改ざん・なりすましされた場合 |
| CLI が手元のファイルだけを扱う(ビルド・変換・lint) | ほぼ無し | 低 外部の Brotli・ZIP を読まない限り |
| CLI・サーバを SEA や startup snapshot で配布している | 106074 | 中 UUID・トークンが全プロセスで重複する |
利用者入力のホスト名を dns.resolve* で検査する | 105482 | 中 許可リストのすり抜け |
--permission で権限を絞って動かす | 106071・106073 | 低 Low |
Node.js 20 は 2026-04-30 にサポートが終了した。公式は終了した系列を検証しないため、影響するかどうかの公式の判定は無く、修正版も出ない。
| CVE | Node.js 20 での見込み(未検証の推定) |
|---|---|
| 111497(Brotli) | 影響ありと見るべき 同じ Brotli ライブラリを同梱しており、修正は本家のコミット。20 にだけ欠陥が無い根拠は無い |
| 106069・106072・105482・105481 | 影響ありと見るべき 22 以降の全系列に共通する処理で、22 より古い 20 に入っていない根拠が無い |
| 105483(全角数字) | 影響しない可能性 24・26 に入った回帰のため。ただし未確認 |
| 106074(SEA・snapshot) | 使っていれば要確認 20 では SEA が実験的機能 |
加えて、20 は 2026-04-30 以降に公開された過去の脆弱性(2026 年 6 月・7 月のリリース分)も修正されていない。今回だけを見て「当たらない」と判断しても、未修正の穴は積み上がっている。
20 から 26 へは 6 メジャー分(21〜26)の変更をまたぐ。公式の移行ガイドは v20→v22 と v22→v24 の 2 本があり、v24→v26 のガイドは見つからなかった。まず 24 へ上げ、続けて 26 を試す二段の移行が現実的。
| Node.js | V8 | NODE_MODULE_VERSION | npm | OpenSSL |
|---|---|---|---|---|
| 20.0 | 11.3 | 115 | 9.6 | 3.0 系 |
| 22.0 | 12.4 | 127 | 10.5 | 3.0 系 |
| 24.0 | 13.6 | 137 | 11.0 | 3.5 |
| 26.0 | 14.6 | 147 | 11.12 | 3.5 |
| 分類 | 変更 | 版 | 影響と対処 |
|---|---|---|---|
| ネイティブアドオン | NODE_MODULE_VERSION が 115→147。V8 の C++ API も変更。ビルドに C++20 が必要 | 毎版 | 要再ビルド node-gyp 製の依存はすべて作り直し。prebuild の対応を確認。Node-API 製なら影響は小さい |
| TLS | OpenSSL 3.5 の既定セキュリティレベル 2。2048 bit 未満の RSA・DH 鍵、224 bit 未満の ECC 鍵、RC4 を拒否 | 24 | 接続失敗 古い証明書の社内サーバや機器への TLS 接続が通らなくなる |
| Windows | .bat・.cmd を spawn で直接起動できない(CVE-2024-27980 対策) | 20 の後期・22 | 要修正 shell: true を付ける |
| Windows | 32 bit(x86)版の提供終了 | 23 | 動かない 32 bit Windows は移行先が無い |
| モジュール | import assertions(assert { type: 'json' })を削除 | 22 | 要修正 with { type: 'json' } に書き換え(20 でも動く書き方) |
| モジュール | require() で ESM を読める(require(esm))が既定 | 22.12・23 | 挙動変化 ERR_REQUIRE_ESM を捕まえて分岐するコードが別の経路に入る |
| 削除された API | crypto.createCipher・createDecipher | 22 | 実行時エラー createCipheriv へ |
| 削除された API | util.isString() 等の util.isX() 群・util._extend・util.log | 23 | 実行時エラー typeof・Array.isArray・Object.assign へ。古い依存パッケージで踏みやすい |
| 削除された API | tls.createSecurePair()、fs.Dirent.path(→parentPath)、OutgoingMessage._headers | 24 | 実行時エラー codemod あり |
| 削除された API | fs.F_OK 等(→fs.constants.F_OK)、SlowBuffer、assert.CallTracker | 25 | 実行時エラー |
| 削除された API | http.Server の writeHeader()、_stream_readable 等の内部モジュール、--experimental-transform-types | 26 | 実行時エラー 古いストリーム系の依存に注意 |
| 非推奨(警告) | url.parse()(実行時警告)、組み込み punycode、module.register() | 21〜26 | 警告 WHATWG URL へ |
| CLI フラグ | --experimental-permission → --permission | 24 | 要修正 起動スクリプトの書き換え |
| 同梱物 | Corepack を同梱しなくなった | 25 | 要修正 corepack enable 前提の CI・Dockerfile は npm i -g corepack 等が必要 |
| テスト | テストランナのサブテストを自動で await。既定のレポータが spec | 23・24 | 挙動変化 node:test の書き方次第 |
| 出力文字列 | util.inspect・assert のメッセージ、TLS のエラーコード名の綴り修正 | 23〜26 | 挙動変化 出力やエラーメッセージを文字列で比べるテストが落ちうる |
| SQLite | node:sqlite で外部キー制約が既定で有効、二重引用符の文字列リテラルが無効 | 23 | 挙動変化 |
| プラットフォーム | Linux armv7(32 bit)のバイナリ提供終了、macOS 13.5 以上 | 24 | 動かない 古い Raspberry Pi 等 |
fetch・Web Streams が安定(21)、WebSocket クライアントがフラグ不要(22)tsconfig.json は読まず、enum 等の型以外の構文は不可node --run(package.json の scripts 実行)、--watch が安定(22〜23)localStorage・sessionStorage(25)、URLPattern(24)using(明示的な資源管理)・RegExp.escape・Float16Array(24)、Temporal が既定で有効(26)node --trace-deprecation を付けて 20 のまま動かし、非推奨の警告を洗い出す(削除される API の多くは 20 でも警告が出る)npm ls で node-gyp・prebuild を使う物).cmd 起動を重点的に確かめるfetch で外部へ取りに行く処理node:20 等のタグを固定していないか)、Electron、CI のランナ、ディストリのパッケージ(libbrotli も)Content-Encoding: br を前段(リバースプロキシ等)で拒否するfetch する処理は、接続先を許可リストに限るconst net = require('node:net');
const url = require('node:url');
// 非正規の IP 表記(0127.0.0.1 など)を拒否する
function isNonCanonicalIP(host) {
return !net.isIP(host) && net.isIP(url.domainToASCII(host)) !== 0;
}
node -v
node -p "process.versions.brotli + ' / ' + process.versions.openssl"
Node.js 本体ではなく、npm パッケージ shell-quote の脆弱性。シェルのコマンド文字列を組み立てる (quote())・分解する (parse()) ためのパッケージで、多くのツールに間接依存として入っている。
| CVE | 深刻度 | 対象 | 修正版 | 公開 |
|---|---|---|---|---|
| CVE-2026-9277 | High CVSS 3.1: 8.1 (4.0: 9.2) | 1.1.0〜1.8.3 | 修正 1.8.4 | 2026-05-22 |
| CVE-2021-42740 | Critical CVSS 3.1: 9.8 | 1.7.3 未満 | 修正 1.7.3 | 2021-10 |
quote() は、演算子を表すオブジェクト ({ op: ';' } など) の .op を、正規表現 /(.)/g で 1 文字ずつエスケープしていた。JavaScript の . は改行に一致しないため、.op の中の改行がエスケープされずに残る。POSIX シェルは改行をコマンドの区切りと読むので、改行の後ろが別のコマンドとして実行される。
const { quote } = require('shell-quote');
const s = quote([{ op: ';\nid' }]); // 改行がそのまま残る
require('child_process').execSync(s, { shell: '/bin/sh' }); // id が実行される
1.8.4 では .op を正しい演算子の一覧と照合し、それ以外を受け付けない。
quote() に渡し、その結果を exec 等でシェルに渡す。parse(cmd, envFn) の envFn がオブジェクトを返す使い方も経路になりうるquote() に渡す一般的な使い方。公表された条件は、オブジェクトのトークンに外部の値が入る場合に限られるNode.js の版とは関係が無い。Node.js 20 でも 26 でも、パッケージを上げれば直る。
npm ls shell-quote # どの依存から入っているか
npm update shell-quote # 宣言の範囲内なら lockfile の更新で 1.8.4 になる
間接依存の範囲が 1.8.4 を許さないときは、package.json の overrides (pnpm は pnpm.overrides) で上書きする。Debian・Ubuntu のパッケージ (node-shell-quote) は、各ディストリの更新 (Debian は DSA-6300-1) で直る。
根本的には、外部の値を含むコマンドをシェル経由で実行しない。execFile・spawn に引数を配列で渡せば、シェルを通らず、この種の注入は起きない。