「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判定を行ってください。
- オリジン保護: アプリ(オリジン)へは、信頼できる前段(CDNやLB)以外からは絶対にアクセスさせない。
- 信頼できるヘッダー: その前段サーバーが付与する「改ざん不可能な独自ヘッダー」だけを見る。
この2つはセットです。オリジンがインターネットに全公開されている状態では、どんなヘッダーを見ようと直アクセスで偽装されてしまいます。
以下に、主要な構成ごとの「正解」コードと必須となる構成要件を提示します。
1. 最上位が AWS CloudFront の場合
最も一般的な構成ですが、実はオリジン保護の難易度が少し高いパターンです。
【前提構成】
CloudFront → ALB (Global IPあり) → EC2/Fargate (Global IPなし)
【必須となるオリジン保護】 ALBがインターネットに公開されているため、以下の2つの組み合わせでALBを保護してください。
- マネージドプレフィックスリスト: Security GroupでCloudFrontのIP帯以外からのアクセスを拒否する。
- カスタムヘッダ認証: 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 Hosting → Next.js等 (Global IPなし)
【必須となるオリジン保護】 バックエンドのアプリ自体がグローバルIPを持たない(外部から直接到達できない)ため、アーキテクチャレベルで保護されています。
【正解コード】 Firebaseがエッジで付与する以下のヘッダーを利用してください。
// ✅ 推奨
const clientIp = req.headers['x-fah-client-ip'];
3. 最上位が Cloudflare の場合
VPSなどを利用する場合でも、Tunnelを使うことでオリジンを隠蔽できます。
【前提構成】
Cloudflare → VPS/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点を徹底してください。
- 怪しい入力値(XFF)は最初から見ない。
- インフラ構成でオリジンへの直アクセスを物理的・論理的に遮断する。
「信頼できるサーバー(オリジン保護あり)」から渡された「信頼できるヘッダー」だけを見る。この原則を守ることこそが、深夜の緊急呼び出しからあなたを守る唯一の方法です。
コメントを投稿するにはログインが必要です。