事故図鑑
X-Forwarded-For(XFF)偽装とは — 信頼するプロキシ設定の落とし穴と防御
X-Forwarded-For(XFF)はクライアントが自由に詐称できるHTTPヘッダです。偽装したXFFにSQLインジェクションやXSSの探索を仕込む自動スキャンと、『すべてのプロキシを信頼する』設定の落とし穴を、個人開発でよくある事例として防御目線で解説します(攻撃手順は載せません)。多層防御がなぜ効き、それでも何を直すべきだったのかを、対症療法と根本対策に分けてまとめます。
X-Forwarded-For(XFF)は、プロキシ経由でも「本当の訪問者IPはこれ」とアプリに伝えるためのヘッダです。ところがこの値はクライアントが自由に詐称できます。個人開発でよくある「XFF偽装スキャンを受けた」事例を、固有名詞を伏せて教訓に変えます(攻撃の再現手順は載せません)。
- 種別
- X-Forwarded-For 偽装 + インジェクション探索(自動スキャン)
- 実害
- なし(多層防御で不成立)
- 気づき方
- 攻撃リクエストが500になり、例外通知メールが大量に飛んだ
- 効いた盾
- ①プレースホルダ(値のバインド)②DBの文字コード検証
- 残っていた甘さ
- 「すべてのプロキシを信頼」する設定(前段にプロキシは無い構成)
- 対処
- 境界でIPヘッダをサニタイズ(対症)→ 信頼するプロキシの適正化(根本)
まず切り分ける:「攻撃」か「自分のミス」か
エラー通知を見て即「攻撃だ」と決めつけないのが鉄則です。直前に構成変更をしていれば、自分の変更を疑う線も同じ重みで検討します。
自分の変更を疑う
値の中身を証拠にする
標的型かスキャンか
ITDの視点:症状から原因を逆算するときは“データで潰す”
「最近いじった所が怪しい」という直感は出発点として有効です。ただし、証拠(この場合は値の中身)が否定するなら手放す。複数の仮説を立て、思い込みでなくデータで一つずつ潰すと、誤診が激減します。
攻撃の正体:XFF偽装にインジェクション探索を相乗り
攻撃ボットはXFFに「IPの代わりに」探索用の文字列を詰め、末尾だけ正常風のIPにします。狙いは一つ——
「このアプリ、XFFの値を信じて、DBクエリに生で混ぜたり、HTMLにそのまま出したりしないか?」
もしアプリが雑なら、XFF経由で SQLインジェクション や XSS が刺さります。これは「IPを運ぶ場所」を任意の文字列の注入口として悪用する手口です。
なぜ実害ゼロだったか:二枚の盾
盾① 値のバインド(プレースホルダ)
IP値を文字列連結せず、プレースホルダでデータとしてDBへ渡していた。入力がSQLの「命令」に化ける余地がない → SQLiは構造的に不成立。フレームワークを使う最大の恩恵の一つ。
盾② DBの文字コード検証
ペイロードの不正バイト(overlong UTF-8 等)は、カラムの文字コードではそもそも保存できない並び。DBが「無効」と判断して書き込みを拒否し、例外を投げた → 攻撃は失敗し、しかも検知できた。
起きたのは「攻撃者自身のリクエストが500になり、例外通知が大量に飛んだ」だけ。漏えい・改ざん・不正ログインはいずれもなし。多層防御が想定どおり機能した好例です。
本当の問題:プロキシを“全部”信頼していた
ここで終われば美談ですが、見過ごせない点があります。そもそも、なぜ偽XFFがIP判定まで届いたのか?
✗ 全プロキシ信頼(ワイルドカード)
クライアントの偽XFF → 「本物のIP」として採用 → ログ・セッション・IP判定が汚染
✓ 何も信頼しない(初期値)
偽XFFは無視 → 直接接続元の実IPを採用 → 詐称が効かない
この構成はリバースプロキシもCDNも挟まないWebサーバー直結でした。つまりXFFを信頼する必要が無いのに「全部信頼」にしていた。「とりあえず全許可」は、開発時の手抜きがそのまま本番に残る典型です。
盲点:XFF偽装の害は“injectionだけ”じゃない
クライアントIPを信じる処理は、すべて汚染されます。レート制限のすり抜け・IP許可リストの突破・BAN回避・監査ログの偽装——インジェクション防御が固くても、ここは別腹です。「IPを信じている場所」を棚卸しして、その信頼の根拠(=プロキシ設定)を確認してください。
対策:境界でサニタイズ → 信頼を適正化
境界でIPヘッダをサニタイズ(対症・即効)
信頼するプロキシを適正化(根本)
http/https 判定にも関わるため、変更でリダイレクトループ等の副作用が出うる → まず1環境で先行検証。“妥当なIP”を“本物のIP”と混同しない
検証:直したら攻撃を再現して確かめる
「直したつもり」を「確かに止まった」に変えます。①対策を1環境にだけ適用 → ②同じ不正ヘッダを自分で送る → ③エラー件数が増えないことを確認 → ④正常アクセスも従来どおり動くことを確認 → ⑤問題なければ全環境へ。段階適用が事故を防ぎます。
続編:IPを塞いだら User-Agent を突かれた(モグラ叩きの罠)
サニタイズを入れて数十分後、また同じ種類のエラーが来ました。今度壊れていたのは別のカラム——ip_address ではなく user_agent です。IPヘッダを弾かれた攻撃者が、同じ不正ペイロードを User-Agent ヘッダに載せ替えてきたのです。ここで発想を変える必要があります。
正しい問いは「どのヘッダが危険か」ではなく「信頼できない値が、最終的にどのDBカラムに保存されるか」です。保存先が ip_address と user_agent の2つだけなら、その2つに届く入口(IP系ヘッダ+User-Agent、念のため Referer も)を浄化すれば、セッション由来のエラーは原理的に止まります。保存されない他のヘッダは横展開できません。正規の User-Agent は ASCII/正常なUTF-8 なので、無効バイトだけを落とす処理を足しても無影響です。
ITDの視点:出口から逆算する(入口を1つずつ潰さない)
ヘッダを1個ずつ塞ぐのはモグラ叩き——攻撃者が使える入口は無数にあります。出口(保存先カラム・出力先)から逆算して、そこに至る入口を一度に全部洗い出すのが正解。「IPを塞いだら User-Agent を突かれた」で学んだとおり、対症療法でヘッダを塞ぐときは同じ保存先に届く他の入口も同時に塞ぐ。そして2回目も、再現して止まったことを確認してから展開しました。
さらに続編:入口で防ぎきれず、出口(保存直前)で守った
User-Agent も塞ぎ「今度こそ」と思った直後、また ip_address カラムで同じエラーが来ました。入口のサニタイズはデプロイ済みで、攻撃と同じ不正ヘッダを自分で送るテストでは再現しない。なのに本番では再発する——ここがこの話の山場です。
理由は、フレームワークの「クライアントIP判定」が内部で複雑だったこと。複数のヘッダ表記(X-Forwarded-For・Forwarded・独自ヘッダ)、プロキシ信頼設定との組み合わせ、パース順やキャッシュ……これらの内部経路のどこかで、こちらが浄化したつもりの値とは別ルートから汚染値が「クライアントIP」として復活していました。入口で全部は捕まえきれないのです。
✗ 入口で守る
リクエストのヘッダを浄化 → 内部の別経路でクライアントIPが復活 → すり抜け
✓ 出口で守る
DB保存の直前で検査 → 途中でどう汚れても“必ず通る一点” → すり抜けなし
そこで発想を180度変え、出口(DBに書く一歩手前)で守ることにしました。セッション保存クラスを継承し、保存値を作る部分だけ差し替えます(概念):保存用IPは「妥当なIPでなければ null」、保存用User-Agentは「不正バイトを除去し長さを制限」。クライアントIP判定が内部でどう転んでも、DBに書く直前で必ず検査を通るので、すり抜けようがありません。攻撃2経路を十数回ずつ連打してもエラーは増えず、正規ユーザーのセッションも正常でした。
ITDの視点:最後の砦は“出口(使う場所)”
セキュリティの定石は「入力を信用するな、使う場所(出口)で必ず検証・エスケープしろ」。入口の検査は“早めに弾く”ために有効ですが、フレームワークが内部で値を解決・加工する種類の情報(クライアントIP判定など)は、入口をすり抜けて復活しえます。DB・HTML・コマンドなど危険な場所へ渡す直前=必ず通る一点で検証・エスケープするのが最後の砦。入口と出口は別物として両方やります。
いちばんの学び:うるさく弾かれる方が安全
今回、攻撃は失敗したうえに大量のエラー通知という形で可視化されました。皮肉なようで、これは悪くない。静かに通り抜けられるより、うるさく弾かれるほうが安全です。エラー通知を「ノイズ」と切り捨てず、「異常検知のセンサー」として活かしたい——「実害は防げた」で終わらせず、「なぜ内部まで届いたのか」を直すのがITDの立場です。
次に読む
- 用語:SQLインジェクションとは / XSSとは / SSRFとは
- 入門:セキュリティ超入門:.env と APIキー
よくある質問
QX-Forwarded-For は信用できる?
そのままでは信用できません。XFFはクライアントが自由に値を入れられるヘッダで、攻撃者は本物らしいIPにも、インジェクション探索の文字列にも詐称できます。信頼できるのは『自分が前段に置いた、信頼するプロキシが付けたXFF』だけです。
QリバースプロキシやCDNを使っていない場合はどうする?
XFFを一切信頼しない(フレームワークの初期値のまま)にします。前段にプロキシが無いのに『すべてのプロキシを信頼』する設定にすると、誰でも偽のIPをアプリに食わせられます。構成が無いなら何も信頼しないのが正解です。
QIPヘッダをサニタイズすれば安全?
インジェクション探索やエラーの洪水は止まりますが、それは衛生処理(出血を止める対症療法)です。『妥当なIP』に整形できても『本物のIP』である保証はありません。本物のIPの信頼は、あくまで信頼するプロキシの設定で担保します。
QIPヘッダを直したのに、また同じエラーが出ました。なぜ?
同じ不正ペイロードが別の入口(例:User-Agentヘッダ)に載せ替えられ、同じ保存先カラムに届いたためです。ヘッダを1つずつ塞ぐとモグラ叩きになります。『信頼できない値が最終的にどのDBカラム/出力先に届くか』から逆算し、そこに至る入口(IP系+User-Agent等)をまとめて浄化してください。