CVE-2026-40175 は,axios (< 1.15.0) が Prototype Pollution のガジェットとして機能し,CRLF ヘッダインジェクションを経由して HTTP リクエストスマグリングを可能にする脆弱性です.
初めは CVSS 10.0 と評価され非常に大きな話題を呼び,私もあわててアナウンスしましたが,公表から数日後に 4.8 に引き下げられました.
この記事では PoC を構築して攻撃の流れを説明します.
攻撃の概要
この脆弱性は単体で成立するものではなく,複数の脆弱性を連鎖させる「ガジェットチェーン」です.
- Prototype Pollution:
lodash.merge等の既知の脆弱性を通じてObject.prototypeを汚染する - axios のヘッダ取り込み: axios がリクエストを組み立てる際,汚染されたプロパティを HTTP ヘッダとして取り込む
- CRLF インジェクション: ヘッダ値に含まれる
\r\nによって HTTP リクエストが分割される - リクエストスマグリング: 分割された 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.4 は CVE-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 の rawTcpAdapter | for...in | 汚染プロパティを拾う |
| axios default http adapter | headers.toJSON() → Object.entries | 自身のプロパティのみ |
Node.js http.request | Object.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.1 | Node 10.24.1 | Node 18.16.0 | Node 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 つの防御を同時にバイパスしています.
for...inによるヘッダイテレーション →Object.prototypeの汚染プロパティを拾う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 チームの疲弊が最近問題になりつつありますが,今回の件もその一例と言えるかもしれません.