2026-04-19 #security

CVE-2026-40175 の HTTP Request Smuggling を検証した


CVE-2026-40175 は,axios (< 1.15.0) が Prototype Pollution のガジェットとして機能し,CRLF ヘッダインジェクションを経由して HTTP リクエストスマグリングを可能にする脆弱性です.

初めは CVSS 10.0 と評価され非常に大きな話題を呼び,私もあわててアナウンスしましたが,公表から数日後に 4.8 に引き下げられました.
この記事では PoC を構築して攻撃の流れを説明します.

検証のために作成したコードはこちら

攻撃の概要

この脆弱性は単体で成立するものではなく,複数の脆弱性を連鎖させる「ガジェットチェーン」です.

  1. Prototype Pollution: lodash.merge 等の既知の脆弱性を通じて Object.prototype を汚染する
  2. axios のヘッダ取り込み: axios がリクエストを組み立てる際,汚染されたプロパティを HTTP ヘッダとして取り込む
  3. CRLF インジェクション: ヘッダ値に含まれる \r\n によって HTTP リクエストが分割される
  4. リクエストスマグリング: 分割された 2 番目のリクエストが内部 API に到達し,機密データが窃取される

PoC の構成

Docker Compose で以下の 3 コンポーネントを構築しました.

  • Internal API (port 3001): 外部非公開の内部サービス./secret で機密データを返す
  • Victim Server (port 3000): 外部公開.lodash@4.17.4_.merge による Prototype Pollution 脆弱性あり,axios@1.14.0 を使用
  • Attacker: ホスト側から攻撃スクリプトを実行

Step 1: Prototype Pollution

Victim Server の PUT /api/settings エンドポイントは,ユーザ入力を lodash.merge でマージします.
lodash@4.17.4CVE-2018-3721 の影響を受けるため,constructor.prototype パスを辿って Object.prototype を汚染できます.

一見すると普通の JSON ですが,lodash.merge がプロトタイプチェーンを辿ることで Object.prototype["x-custom-header"] が設定されます.
これ以降,このプロセス内で生成されるすべてのオブジェクトが x-custom-header プロパティを持つようになります.

Step 2: CRLF インジェクションによるリクエスト分割

汚染後に Victim Server 経由で内部 API にリクエストを送ると,axios がヘッダをイテレートする際に汚染プロパティを拾います.
結果として,TCP 上では以下のように 2 つの HTTP リクエストが送信されます.

GET /health HTTP/1.1          ← 正常なリクエスト
Host: internal-api:3001
Accept: application/json
x-custom-header: dummy        ← ここで CRLF がリクエストを分割
                               ← (空行 = ヘッダ終了)
GET /secret HTTP/1.1          ← スマグリングされたリクエスト
Host: internal-api:3001
Connection: close

外から見ると /health への無害なリクエストですが,裏では /secret にもアクセスしてしまいます.

なぜ CVSS が 10.0 ではなく 4.8 なのか

PoC を動かすと確かに攻撃は成立するのですが,実は PoC ではカスタムの TCP adapter を使用しています
ここが CVSS スコアに大きく影響するポイントです.

実環境での 2 つの防御層

デフォルトの http/https adapter を使う一般的なアプリケーションでは,2 つの独立した防御層が攻撃を阻止します.

防御層 1: axios の AxiosHeaders.toJSON()

axios 1.14.0 の http adapter は,ヘッダを AxiosHeaders.toJSON() でシリアライズしてから Node.js の http.request に渡します.

// axios/dist/node/axios.cjs:3019
const options = {
  headers: headers.toJSON(),  // own properties のみ
  // ...
};

toJSON() は自身のプロパティ(own properties)のみを返すため,Object.prototype に注入されたプロパティはリクエストヘッダに含まれません.

一方,PoC のカスタム adapter は for...in でヘッダをイテレートしており,これはプロトタイプチェーンを辿るため汚染プロパティを拾ってしまいます.

実装イテレーション方法Prototype 汚染の影響
PoC の rawTcpAdapterfor...in汚染プロパティを拾う
axios default http adapterheaders.toJSON()Object.entries自身のプロパティのみ
Node.js http.requestObject.keys自身のプロパティのみ

防御層 2: Node.js の setHeader() による CRLF 検証

仮にヘッダが拾われたとしても,Node.js の http.ClientRequest.setHeader() が CRLF を含むヘッダ値を TypeError で拒否します.

実際に Node 4.9.1 から Node 20.20.2 まで幅広いバージョンで検証しましたが,すべてのバージョンで CRLF は拒否されました.
この検証は CVE-2016-2216 の修正により Node 4.x 以降で有効になっています.

Node 0.11 だと通るらしいという検証結果がある.
さすがにそんな古いバージョンを使っているケースは少ないか?

ペイロードNode 4.9.1Node 10.24.1Node 18.16.0Node 20.20.2
Standard CRLF (\r\n)拒否拒否拒否拒否
Lone CR (\r)拒否拒否拒否拒否
Lone LF (\n)拒否拒否拒否拒否
CR+LF+CR (\r\n\r) CVE-2025-23167 風拒否拒否拒否拒否
Unicode U+2028拒否拒否拒否拒否

関連する Node.js の脆弱性との関係

検証の過程で,Node.js の HTTP パーサに関する以下の脆弱性についても調査しました.

  • CVE-2023-30589: llhttp が単独 CR をヘッダ区切りとして受理(Node < 18.16.1)
  • CVE-2025-23167: \r\n\rX による不正なヘッダ終端(Node 20.x < 20.19.2)

これらは HTTP パーサ(受信側) の脆弱性です.今回の CRLF インジェクションはリクエストの 送信側 の話なので,setHeader() の検証とは別のレイヤーになります.
つまり,これらの脆弱性があるバージョンでも送信側の防御は機能しており,組み合わせて攻撃を成立させることはできませんでした.

PoC のカスタム adapter が必要な理由

PoC の rawTcpAdapter は,上記 2 つの防御を同時にバイパスしています.

  1. for...in によるヘッダイテレーション → Object.prototype の汚染プロパティを拾う
  2. net.Socket による生 TCP 書き込み → setHeader() の CRLF 検証をスキップ

一般的なアプリケーションでこのような実装をすることはまずないため,実際の攻撃成立条件は相当限定的です.

まとめ

CVE-2026-40175 は,ガジェットチェーンとして組み合わせた場合のインパクトは大きいものの,デフォルトの adapter では axios の toJSON() と Node.js の setHeader() という 2 層の防御により攻撃が成立しません.
かなり古い Node.js でなければ,カスタム TCP adapter を使わない限り攻撃は通らない.CVSS 10.0 という評価は過剰であり,axios チームがどのような検証フローを経て,この CVE を公開したのかは気になるところです.

AI エージェントの発展により,PR が乱立し,OSS チームの疲弊が最近問題になりつつありますが,今回の件もその一例と言えるかもしれません.