1. なぜ起きるか #why
HTTP/1.1 でリクエストの長さを示す方法は2つあり、どちらか一方が使われる:
- Content-Length(CL) — バイト数を明示
- Transfer-Encoding: chunked(TE) — チャンク分割で送信、最後は
0\r\n\r\n
仕様上、両方が指定された場合は TE を優先する。しかし実装によって扱いが違う。フロントエンドが CL を、バックエンドが TE を採用すると、両者が認識する「リクエストの終わり」がずれる。
2. CL.TE 攻撃 #cl-te
フロントが Content-Length、バックエンドが Transfer-Encoding を採用するパターン。
POST / HTTP/1.1 Host: shop.utsuroi.example.com Content-Length: 13 Transfer-Encoding: chunked 0 SMUGGLED_REQ ↑ ここまでが攻撃者の HTTP リクエスト
フロントは Content-Length=13 と判断し、空行から13バイト分(0\r\n\r\nSMUGGLED_REQ)を「1つのリクエスト本文」として扱う。一方バックエンドは Transfer-Encoding を見て、0\r\n\r\n でリクエスト終了と判断 → その後の SMUGGLED_REQ を「次のリクエストの先頭」と解釈。
これによって、攻撃者の「smuggled」リクエストが、後続の正規リクエストの先頭に「結合」される事態が起きる。
3. TE.CL 攻撃 #te-cl
フロントが Transfer-Encoding、バックエンドが Content-Length のパターン。逆方向だが原理は同じ。
近年は Transfer-Encoding 自体を標準化された形でしか受け付けない実装が増え、TE.TE 系(両者が TE を見るが、いずれかが「曖昧な TE ヘッダ」で混乱)の攻撃も頻出。
4. 何ができるか #impact
- 他ユーザのリクエストを覗く — smuggled リクエストの「本文」として、後続の正規リクエストの一部が結合される。攻撃者がセッション Cookie 等を入手可能
- Front-end の認可をバイパス — Front-end の認可チェックが効かないバックエンド直送リクエストを送る
- キャッシュ汚染 — Front-end のキャッシュに smuggled レスポンスを保存させる
- XSS の量産配信 — 全ユーザに同じ攻撃を配信
5. 正しい防御 #defense
- HTTP/2 をエンドツーエンドで使う — HTTP/2 は明確なフレーム長ベースなので、CL/TE の曖昧さがない。HTTP/2 が一般化した今、最も有効な対策
- あいまいなリクエストを拒否 — Content-Length と Transfer-Encoding が両方ある、TE が複数ある、改行コードが奇妙、等のリクエストはフロントエンドで 400 を返す
- 同じ実装で揃える — フロントとバックエンドで同じ HTTP パーサ実装を使う(困難な場合が多い)
- 1接続1リクエスト — Connection: close を強制(パフォーマンス低下)
- Front-end のバージョンを最新に — Cloudflare、AWS ALB、nginx、HAProxy 等は smuggling 検出パッチを継続的に出している