【実録】X-Forwarded-Forを絶対に使わない理由 〜「オリジン保護」なきIP制限は無意味である〜

「IP制限を突破されました」

金曜の夜、その報告は突然やってきます。背筋が凍る瞬間ですよね。 調査を進めると、原因はいつも古(いにしえ)からあるあのヘッダー、X-Forwarded-For(以下XFF)にあると私は感じています。

結論から申し上げます。WebアプリケーションでクライアントIPを取得する際、X-Forwarded-Forは絶対に使ってはいけません。

しかし、単に「見るヘッダーを変える」だけでは不十分です。今回の修正版記事では、多くの技術ブログが見落としがちな**「オリジン保護(Origin Protection)」**という大前提をセットにして、真に安全なIP制限の実装パターンを解説します。


なぜ X-Forwarded-For は危険なのか

偽装の容易さとキャッシュ事故

世の中には「XFFの右端から信頼できる数だけ戻る」といったパース処理の記事が溢れています。しかし、攻撃者が X-Forwarded-For: 127.0.0.1 のようなヘッダーを付与してリクエストを送った場合、アプリやWAFがそれを安易に信じてしまう事故が後を絶ちません。

また、IP制限の判定ミスにより、本来Forbiddenになるべきリクエストが「200 OK」としてCDNにキャッシュされ、世界中に公開されてしまう「キャッシュ汚染」も頻発しています。

**「偽装可能な値を入力として受け入れている」**時点で、セキュリティとしては敗北しているのです。


大原則:信頼できるサーバーからの「確実な通信」のみを信じる

解決策はシンプルです。以下の2つの条件が揃ったときのみ、IP判定を行ってください。

  1. オリジン保護: アプリ(オリジン)へは、信頼できる前段(CDNやLB)以外からは絶対にアクセスさせない。
  2. 信頼できるヘッダー: その前段サーバーが付与する「改ざん不可能な独自ヘッダー」だけを見る。

この2つはセットです。オリジンがインターネットに全公開されている状態では、どんなヘッダーを見ようと直アクセスで偽装されてしまいます。

以下に、主要な構成ごとの「正解」コードと必須となる構成要件を提示します。

1. 最上位が AWS CloudFront の場合

最も一般的な構成ですが、実はオリジン保護の難易度が少し高いパターンです。

【前提構成】 CloudFrontALB (Global IPあり)EC2/Fargate (Global IPなし)

【必須となるオリジン保護】 ALBがインターネットに公開されているため、以下の2つの組み合わせでALBを保護してください。

  1. マネージドプレフィックスリスト: Security GroupでCloudFrontのIP帯以外からのアクセスを拒否する。
  2. カスタムヘッダ認証: CloudFrontから特定の秘密ヘッダー(例: X-Origin-Secret)を送り、ALB側でその値を検証する。

【正解コード】 XFFは無視し、CloudFrontが保証するヘッダーを利用します。 ※ CloudFrontのOrigin Request Policyにこのヘッダーを含める必要があります。

// ✅ 推奨:CloudFrontが検証済みのIPのみを信じます
const ip = req.headers['cloudfront-viewer-address'];

// ※ ポート番号が含まれる場合があるので注意(例: 192.0.2.1:12345)
const clientIp = ip ? ip.split(':')[0] : null;

2. 最上位が Firebase App Hosting の場合

Googleのエコシステムで完結する場合、構成は非常にシンプルかつ堅牢です。

【前提構成】 App HostingNext.js等 (Global IPなし)

【必須となるオリジン保護】 バックエンドのアプリ自体がグローバルIPを持たない(外部から直接到達できない)ため、アーキテクチャレベルで保護されています。

【正解コード】 Firebaseがエッジで付与する以下のヘッダーを利用してください。

// ✅ 推奨
const clientIp = req.headers['x-fah-client-ip'];

3. 最上位が Cloudflare の場合

VPSなどを利用する場合でも、Tunnelを使うことでオリジンを隠蔽できます。

【前提構成】 CloudflareVPS/VM (Global IPあり)

【必須となるオリジン保護】 Cloudflare Tunnel (cloudflared) を利用してください。これにより、VPSのInboundポートを全開放する必要がなくなり、Cloudflare経由のトラフィックのみを受け入れることができます。

【正解コード】

// ✅ 推奨
const clientIp = req.headers['cf-connecting-ip'];

4. 最上位が Nginx (リバースプロキシ) の場合

自前のNginxを最前線に置くクラシックな構成です。

【前提構成】 Nginx (Global IPあり)Appサーバー (Apache/PHP-FPM/Node.js等 - Global IPなし)

【必須となるオリジン保護】 Appサーバーはプライベートネットワーク内に配置し、インターネットからの直接アクセスを持たせない(Global IPなし)構成にしてください。

【正解コード】 Nginx側で接続元IP($remote_addr)を独自のヘッダーにセットし、アプリ側はその独自ヘッダーのみを見ます。

Nginx側の設定:

# アプリ専用の「信頼できるヘッダー」を作ります
proxy_set_header X-Real-IP $remote_addr;

アプリ側の実装:

// ✅ アプリはNginxがセットしたこのヘッダーだけを信じます
// Nginxより前の経路で何が入っていても、$remote_addrで上書きされているため安全です
const clientIp = req.headers['x-real-ip'];

結論:シンプルさと「構成」で守る

「X-Forwarded-Forを正しくパースする」という努力は、事故の元です。以下の2点を徹底してください。

  1. 怪しい入力値(XFF)は最初から見ない。
  2. インフラ構成でオリジンへの直アクセスを物理的・論理的に遮断する。

「信頼できるサーバー(オリジン保護あり)」から渡された「信頼できるヘッダー」だけを見る。この原則を守ることこそが、深夜の緊急呼び出しからあなたを守る唯一の方法です。



コメント (0)

コメントを投稿するにはログインが必要です。