リクエストターゲットを制する者はHTTPを制す:RFC 7231が定める「宛先」の深淵
ネットワークエンジニアとして現場に立っていると、ふと「HTTPリクエストの1行目」の重要性を忘れてしまいがちだ。`GET /index.html HTTP/1.1`。このたった一行に、現代のWebインフラを揺るがすセキュリティの火種や、API設計の美学が詰まっている。
今日は、RFC 7231が定義する「リクエストターゲット」の4つの形式を紐解きながら、なぜ「URIの正規化」が現場のエンジニアにとっての生命線なのかを語ろう。
—
1. リクエストターゲットの4つの顔
HTTPリクエストの開始行(Request Line)において、パスやURIを指定する箇所を「リクエストターゲット」と呼ぶ。状況に応じて使い分けられるこの4つの形式を、まずは整理しよう。
① オリジン形式 (origin-form)
最も一般的だ。`GET /api/v1/users HTTP/1.1` のように、絶対パスとクエリ文字列のみを指定する。
- 用途: 通常のブラウザからサーバーへの通信、つまり「同じオリジン」内でのリクエスト。
- 注意点: ホスト名が含まれないため、サーバー側は `Host` ヘッダーを頼りに処理を振り分けることになる。
② 絶対URI形式 (absolute-form)
`GET http://api.example.com/v1/users HTTP/1.1` のように、プロトコルからパスまで全てを書く。
- 用途: 主にプロキシサーバー経由の通信。プロキシが「どこに転送すべきか」を判断するために使われる。
③ 権限形式 (authority-form)
`CONNECT api.example.com:443 HTTP/1.1` といった形式だ。
- 用途: `CONNECT` メソッド専用。HTTPS通信をプロキシ経由でトンネリングする際に使用する。これ以外で見かけることはまずない。
④ アスタリスク形式 (asterisk-form)
`OPTIONS HTTP/1.1` のように、ターゲットを “ 一文字にする。
- 用途: サーバー全体(特定のパスではなくサーバーそのもの)に対して機能を確認する際、主に `OPTIONS` メソッドで使われる。
—
2. 現場を泣かせる「URI正規化」の罠
さて、ここからが本題だ。APIゲートウェイやリバースプロキシを構築する際、最も慎重になるべきが「URIの正規化(Normalization)」である。
外部から送られてくるURIは、一見クリーンに見えても、実態はカオスだ。`..`(ディレクトリトラバーサル)、`//`(重複スラッシュ)、`%2e%2e`(エンコードされた攻撃)など、攻撃者はあらゆる手段でサーバーの深い階層に潜り込もうとする。
トラブルシューティングの勘所
正規化を怠ると、例えば `/api/config/` と `/api/config/../secret` が同一視され、本来アクセス禁止であるはずのパスへ到達できてしまう。
実務での防御策:
1. 正規化の順序: デコード(`%xx`の復元)を先に行い、その後にパスの解決(`..`の削除)を行う。この順番を逆にすると、二重エンコード攻撃の餌食になる。
2. プロキシの挙動: NginxやApacheの設定で `merge_slashes` が有効か確認すること。デフォルトでONになっていることが多いが、アプリケーション側でパスを解釈する際に予期せぬ挙動を招くことがある。
—
3. 実践:デバッグのためのツールキット
実際にパケットがどう飛んでいるかを確認するために、以下のスニペットを活用してほしい。
curlでターゲット形式を制御する
通常の `curl` はオリジン形式を送るが、プロキシを通す場合は挙動が変わる。デバッグ時に役立つのは `-v` オプションだ。
権限形式を確認したい場合、プロキシを通してみる
実際にどういうリクエストが送られているかヘッダーを確認
curl -v -x http://your-proxy:8080 http://example.com/test
Pythonで「正規化」をシミュレートする
API開発において、リクエストを受け取った後にパスを解決するロジックは以下のようになる。
from urllib.parse import urlparse, urljoin, posixpath
def normalize_path(path):
# パス内の重複スラッシュを削除し、親ディレクトリ指定を解決する
# これはセキュリティの第一歩
return posixpath.normpath(path)
テストケース
path_input = “/api/v1//users/../settings”
print(f”入力: {path_input}”)
print(f”正規化後: {normalize_path(path_input)}”)
結果: /api/settings となり、攻撃的なパスが封じられる
—
4. シニアエンジニアからの提言
若手エンジニアから「正規化はフレームワークが勝手にやってくれるのでは?」という質問を受けることがある。答えは「半分正解で、半分は危険」だ。
フレームワークは「ルーティング」のために正規化を行うが、インフラ層(NginxやWAF)でも同様の正規化を行わないと、「WAFは通過したのに、アプリ層で解釈が変わって攻撃が成立する」という最悪のケースを招く。
- Tips: ログには「正規化される前の生のリクエストURI」を必ず残せ。正規化後の値だけをログに吐いていると、インシデント発生時に「攻撃者が何を送りつけてきたか」という痕跡が消えてしまう。
HTTPはただの文字列のやり取りではない。それは、クライアントとサーバーの間で交わされる、厳格な「契約」だ。その契約の宛先(ターゲット)を正しく理解し、正規化というガードレールを敷くこと。それが、堅牢なシステムを構築するための唯一の道である。
次は、`Host` ヘッダー偽装によるキャッシュ汚染について深く潜ってみようか。また現場で会おう。
コメント