Hello there, ('ω')ノ
前提:扱うサンプル(行末は \r\n と数えます)
クライアントがフロントに送った文字列(省略せず可視化):
POST / HTTP/1.1\r\n Host: vulnerable\r\n Content-Length: 20\r\n Transfer-Encoding: chunked\r\n \r\n 0\r\n \r\n GET /admin HTTP/1.1\r\n Host: vulnerable\r\n
ポイント:Content-Length: 20 と Transfer-Encoding: chunked が同時に存在している(=矛盾)。
フロント(ここでは「Content-Length を信じる」実装)を仮定します。
ステップ 0 — 用語整理(簡単に)
- クライアント:攻撃者や普通のユーザ。最初にこの長いデータを送る側。
- フロント:ロードバランサ/リバースプロキシ等。クライアントから受け取り、バックエンドへ転送する。
- バックエンド:実アプリが動くサーバ。フロントから受け取ったバイト列を解析する。
- 送信済み部分:フロントがバックエンドに既に送ったバイト列。
- 残り(余り):フロントがまだバックエンドに渡していないバイト列(フロント側のバッファに残っているもの)。
ステップ 1 — フロントが「どこまで」送るか(今回の前提)
Content-Length: 20 を信じるフロントは「本文(body)」をちょうど20バイト送ります。
本文の先頭はヘッダ終端(空行 \r\n)の直後から始まります。
本文(可視化):
0\r\n\r\nGET /admin HTTP/1.1\r\nHost: vulnerable
最初の20バイトは文字列としては次までです:
0\r\n\r\nGET /admin HTTP
^-- ここで切れて('P' が20バイト目)
つまりフロント→バックへ送信済みなのは(ヘッダ +)本文20バイト分まで。残りは以下:
/1.1\r\nHost: vulnerable\r\n
これが「フロントに残っている」状態です。
ステップ 2 — バックエンドが受け取った「送信済み部分」の中身と最初の判断
バックエンドは受け取ったストリーム(ヘッダ+本文20バイト)を順に処理します。重要なのはバックエンド側のパーサの優先ルール:
もしバックエンドが Transfer-Encoding: chunked を優先する実装なら、先頭の
0\r\n\r\nを「空チャンク=本文終了」として解釈します。 → この時点でバックエンドは POST の本文を終了したと判断する。次に流れてくるデータは新しいリクエストの開始だと見なす準備をする。もしバックエンドが Content-Length を優先する実装なら、フロントが送った20バイトをそのまま「POSTの本文」として消費する(=前のリクエストに吸収)。 → 結果として
GET /adminの先頭は POST の本文の一部であり、次のリクエストは壊れる・来ない。
今回「フロントが CL を使って20バイト送った」ケースの最も典型的な/攻撃者が狙う流れは、バックエンドが Transfer-Encoding を優先するパターンです。以下、それを中心に説明します。