X-Forwarded-For とクライアントIPの特定

なぜ元の IP が消えるのか

Web アプリの前には、たいてい [[リバースプロキシ]] や [[ロードバランサ]]、[[CDN]] が並んでいます。これらは受け取ったリクエストを作り直して後段へ送るため、TCP の接続元はプロキシ自身になります。アプリから見た「相手の IP」は、いつも同じプロキシの内部アドレスです。

そこで、プロキシが「私はこの IP から受け取った」とヘッダーに書き添えて渡します。それが X-Forwarded-For(XFF)です。

X-Forwarded-For の形

複数のプロキシを経由すると、各プロキシが右端に追記していきます。

X-Forwarded-For: 203.0.113.5, 198.51.100.7, 10.0.0.2
  • 左端 203.0.113.5 — 最初のプロキシが受け取ったときの接続元
  • 198.51.100.7 — 2つ目のプロキシが受け取ったときの接続元(= 1つ目のプロキシ)
  • 右端 10.0.0.2 — 最後のプロキシが受け取ったときの接続元

つまり右へ行くほど自分に近く、信頼できる値です。左端は「最初に受け取ったプロキシがそう主張している」以上の意味を持ちません。

詐称できるという前提

ここが最大の落とし穴です。X-Forwarded-For はただの HTTP ヘッダーなので、クライアントが最初から付けて送れます。

curl -H "X-Forwarded-For: 127.0.0.1" https://example.com/admin

最初のプロキシが「既にある値の右に追記する」動作をすると、届く値は 127.0.0.1, 203.0.113.5 になります。ここで左端を使う実装は、攻撃者が指定した 127.0.0.1 を本物の送信元と誤認します。「社内 IP だけ許可」「1 IP あたり毎分60回まで」といった制御が、ヘッダー1行で抜けられる状態です。

信頼できる値の取り出し方

原則は信頼するプロキシの数だけ右から数えることです。自分の前に信頼できるプロキシが1台なら、右端の1つ手前が「そのプロキシが実際に受け取った接続元」です。それより左は誰が書いたか分からないので捨てます。

多くのフレームワークにはこのための設定があります。Express なら trust proxy に信頼するホップ数や CIDR を渡し、Nginx なら set_real_ip_from で信頼する送信元を列挙して real_ip_header で読むヘッダーを指定します。設定しないまま左端を読むのが典型的な事故です。

さらに確実なのは、[[CDN]] やロードバランサが自分で判定した値を別ヘッダーで渡す方式です。Cloudflare の CF-Connecting-IP、Akamai の True-Client-IP などがそれで、クライアントが同名のヘッダーを送ってもプロバイダ側が上書きします。ただし、その CDN を経由せず直接オリジンへ届く経路が残っていると、やはり詐称できます。オリジンを CDN 以外から到達不能にして初めて信頼できます。

標準化された Forwarded ヘッダー(RFC 7239)もあり、Forwarded: for=203.0.113.5;proto=https のように1つのヘッダーに複数の情報を持てます。ただし普及度は X-Forwarded-For に及ばず、両方を扱う実装が必要になることが多いです。

レート制限・アクセス制限との関係

[[レートリミット]] の鍵に IP を使う場合、その IP が詐称できるかどうかで防御の意味が変わります。詐称できるヘッダーを鍵にすると、攻撃者はリクエストごとに違う値を送って毎回新しいバケツを手に入れ、制限が事実上なくなります。

安全側の設計は次のとおりです。

  • 鍵にはプロバイダが上書きするヘッダーか、信頼ホップ数で切り出した値だけを使う
  • 信頼できる IP が取れないなら、IP を鍵にしない。APIキーや認証済みユーザーの ID、あるいは経路全体に対する総量上限に切り替える
  • IP 制限を「唯一の認可」にしない。IP は補助的な条件に留め、本命は [[認証と認可]] で守る

初学者向けポイント

  • ログに出ている IP が全部同じなら、プロキシの向こう側を見ています。まず経路上に何台あるかを図に描きましょう
  • 「左端を取る」サンプルコードは検索すると大量に出てきますが、そのままでは詐称に弱いです。信頼するプロキシの数を決めてから右から数えます
  • ローカル開発では経由するプロキシが無いので、本番だけで起きる不具合になりがちです。本番の経路を前提にテストを書きましょう

関連技術とのつながり

  • [[リバースプロキシ]] — ヘッダーを付ける側。X-Forwarded-Host / X-Forwarded-Proto も同じ仲間
  • [[プロキシサーバー]] — 経由するたびに接続元が付け替わる仕組みの基礎
  • [[ロードバランサ]] / [[CDN]] — 経路上の段数を増やす要素。プロバイダ固有のヘッダーの出どころ
  • [[レートリミット]] — 詐称できる IP を鍵にすると制限が無効化される
  • [[IPアドレスとサブネット]] — 信頼する送信元を CIDR で書くための基礎
  • [[HTTP]] — ヘッダーは誰でも付けられる、という出発点
Q: X-Forwarded-For に複数の IP が並んでいるとき、最も信頼できるのはどれ?
- [ ] 左端。最初の送信元だから
- [x] 右端に近い値。自分に近いプロキシが付けた値だから
- [ ] 真ん中。両側の平均だから
解説: 各プロキシが右端に追記するため、右へ行くほど自分に近く、信頼するプロキシが実際に受け取った値です。左端はクライアントが自分で書けます。

Q: レートリミットの鍵に詐称可能なヘッダーの IP を使うと何が起きる?
- [x] リクエストごとに違う値を送られ、制限が事実上なくなる
- [ ] 制限が厳しくなりすぎて正規ユーザーが弾かれる
- [ ] ヘッダーが長すぎてリクエストが失敗する
解説: 鍵を自由に変えられれば毎回新しいカウンタになります。信頼できる IP が取れないなら IP を鍵にしないのが安全側です。

Q: CDN が付ける CF-Connecting-IP のようなヘッダーを信頼できる条件はどれ?
- [ ] ヘッダー名が独自なので誰も知らないから常に信頼できる
- [x] オリジンが CDN 経由以外から到達不能になっている
- [ ] HTTPS で通信している
解説: CDN を迂回して直接オリジンへ届く経路が残っていると、同名ヘッダーを付けて送れば詐称できます。