境界線を曖昧にするな:Content-Lengthが握るWeb通信の「生命線」
ネットワークエンジニアとして現場に立っていると、「HTTPはテキストベースの単純なプロトコルだ」と高を括っている若手がたまにいる。だが、HTTP/1.1の深淵に触れれば触れるほど、それがどれほど危うい幻想であるかを思い知らされるはずだ。
今日取り上げるのは、HTTPメッセージの「境界」を決める絶対的なルール、`Content-Length`ヘッダーだ。たった一行の数値、されどこれが誤れば、システムは崩壊し、セキュリティの穴が空く。なぜこのヘッダーがこれほどまでに重要なのか、そしてなぜそれが「リクエストスマグリング」という悪夢の入り口になるのか、現場の視点から紐解いていこう。
—
1. なぜ「長さ」を伝えなければならないのか?
HTTP/0.9の時代は、サーバーがレスポンスを送った後にコネクションを閉じることで、「ここで通信終了」と合図していた。しかし、HTTP/1.1で導入された「Keep-Alive(持続的接続)」によって世界は変わった。一つのTCPコネクションを使い回す以上、受信側は「どこまでが一つのメッセージで、どこからが次のリクエストか」を正確に判断しなければならない。
ここで登場するのが `Content-Length` だ。
メッセージ境界の判断プロセス
受信側(Webサーバーやリバースプロキシ)は、以下の手順でメッセージを解析する。
1. ヘッダーの終端を見つける: `\r\n\r\n`(空行)までをヘッダーと見なす。
2. Content-Lengthを読み取る: ボディのバイト数を把握する。
3. 正確に読み取る: 指定されたバイト数だけを読み込み、そこでメッセージを区切る。
もしこの数値が実際のボディサイズと一致しなければ、Webサーバーは「次のリクエストの始まり」を誤認する。これが、後述する悲劇の始まりだ。
—
2. リクエストスマグリング:境界の解釈違いが招く災厄
「リクエストスマグリング(HTTP Request Smuggling)」とは、フロントエンド(L7ロードバランサーなど)とバックエンドサーバー間で、メッセージ境界の解釈が食い違うことで発生する攻撃だ。
例えば、攻撃者が以下のような不正なパケットを送りつけたとしよう。
POST / HTTP/1.1
Host: target.com
Content-Length: 13
Transfer-Encoding: chunked
0
SMUGGLED
フロントエンドは `Transfer-Encoding` を優先して処理し、バックエンドが `Content-Length` を信じるとしたらどうなるか。バックエンドは `Content-Length: 13` に従ってメッセージを読み込もうとするが、ボディが途中で終わってしまう。その結果、次に送られてくる正当なユーザーのリクエストが、前のリクエストの「続き」として処理されてしまうのだ。
これが何を意味するか。攻撃者は、他のユーザーのセッションを乗っ取ったり、本来アクセスできない管理画面のパスを裏側で実行させたりすることが可能になる。
—
3. 実践:デバッグと検証のためのTips
現場で「なぜかレスポンスが壊れる」「特定のブラウザからだけ接続が切れる」といった怪奇現象に遭遇したら、まずは `curl` を使って生のパケットを覗くのが鉄則だ。
curlで意図的に「壊れた」リクエストを送る
-v でヘッダーのやり取りを確認し、意図的にContent-Lengthを偽装してみる
curl -v -X POST http://localhost:8080/ \
-H “Content-Length: 5” \
-d “1234567890”
本来10バイト必要なのに5バイトしか指定しない
これにより、残りの5バイトが次のリクエストとしてサーバー内に滞留する可能性がある
Pythonで境界を検証する(ダミーサーバー)
簡単なスクリプトを書いて、自身のサーバーがどう解釈するか確認してみるのもいい。
import http.server
class RequestHandler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
# コンテンツの長さを取得
length = int(self.headers.get(‘Content-Length’, 0))
# 指定されたバイト数だけ読み込む
body = self.rfile.read(length)
print(f”受信したボディサイズ: {len(body)}”)
self.send_response(200)
self.end_headers()
このサーバーは「Content-Length」を信じ切るため、
偽装されたパケットに対してどう反応するかを確認できる
—
4. エンジニアとして守るべき「鉄則」
トラブルシューティングの現場では、以下の3点を意識して設定を確認してほしい。
- ヘッダーの二重指定は避ける: `Content-Length` と `Transfer-Encoding: chunked` が混在する場合、HTTP仕様書(RFC 7230)では `Transfer-Encoding` が優先されると定義されている。しかし、中間のプロキシサーバーがどう解釈するかはベンダー依存だ。極力、どちらか一方に絞る設計にすること。
- 厳格なバリデーション: フロントエンドのインフラ(NginxやAWS ALB)で、リクエストの正当性を厳しくチェックする設定を入れる。`proxy_request_buffering` を有効にして、不正な長さのメッセージをバックエンドに渡さないようにするのが定石だ。
- ログの解像度を上げる: Webサーバーのアクセスログに必ず `request_length` や `upstream_response_length` を出力する。数値のズレは、攻撃の予兆か、あるいは単なる実装バグのサインだ。
—
ネットワークの世界で「なんとなく動いている」は、明日壊れるシステムと同義だ。`Content-Length` のような一見地味なヘッダーの裏側に、通信の整合性を守るための知恵が詰まっている。パケットの流れを想像し、境界を明確に意識する。それが、一流のエンジニアへの近道だ。
さて、次は君のサーバーのログを見てみようか。そこに何が隠れているか、楽しみだね。
コメント