対策・学習15分で読める

X-Forwarded-For(XFF)偽装とは:信頼するプロキシの設定ミスと防御

X-Forwarded-For(XFF)はクライアントが自由に詐称できるHTTPヘッダです。偽装XFFを使う自動スキャンの例と『すべてのプロキシを信頼する』設定の問題点、応急処置と根本対策を解説します。

本文を読む
公開日
更新日
目次

X-Forwarded-For(XFF)は、プロキシ経由でも「本当の訪問者IPはこれ」とアプリに伝えるためのヘッダです。ところがこの値はクライアントが自由に詐称できます。個人開発でよくある「XFF偽装スキャンを受けた」事例を、固有名詞を伏せて教訓に変えます(攻撃の再現手順は載せません)。

事例サマリー — CASE FILE
種別
X-Forwarded-For 偽装 + インジェクション探索(自動スキャン)
実害
なし(多層防御で不成立)
気づき方
攻撃リクエストが500になり、例外通知メールが大量に飛んだ
効いた対策
①プレースホルダ(値のバインド)②DBの文字コード検証
残っていた設定の不備
「すべてのプロキシを信頼」する設定(前段にプロキシは無い構成)
対処
境界でIPヘッダをサニタイズ(応急処置)→ 信頼するプロキシの適正化(根本対策)
0
実害(漏えい・改ざん)
2つ
効いた対策(多層防御)
全許可
プロキシ信頼の設定
境界検証
主な対策

まず切り分ける:「攻撃」か「自分のミス」か

エラー通知を見てすぐに「攻撃だ」と決めつけないことが大切です。直前に構成変更をしていれば、自分の変更を疑う線も同じ重みで検討します。

1

自分の変更を疑う

直近の設定変更で利用者が再ログインを繰り返している、という仮説を立てる。だが、それでは保存値に紛れ込んだ不正バイト列を説明できない → 却下。
2

値の中身を証拠にする

混入していたのはクォートの多重URLエンコードや overlong(冗長)なバイト列。正規の利用者やプロキシが生成する値ではない → 悪意あるペイロードと断定。
3

標的型かスキャンか

特定狙いというより、インターネット全体を無差別に調べる自動スキャナの可能性が高い、と推定する。

当サイトの視点:症状から原因を探るときは、仮説をデータで1つずつ確かめる

「最近いじった所が怪しい」という直感は出発点として有効です。ただし、証拠(この場合は値の中身)が否定するなら手放す。複数の仮説を立て、思い込みでなくデータで一つずつ確かめて否定していくと、原因の取り違えが大きく減ります。

攻撃の内容:XFF偽装とインジェクションの探索

攻撃ボットはXFFに「IPの代わりに」探索用の文字列を入れ、末尾だけ正常に見えるIPにします。狙いは次の1点です。

「このアプリ、XFFの値を信じて、DBクエリに生で混ぜたり、HTMLにそのまま出したりしないか?」

もしアプリが雑なら、XFF経由で SQLインジェクション や XSS が成立します。これは「IPを伝えるヘッダ」を任意の文字列を送り込む場所として悪用する手口です。

被害がなかった理由:2つの対策

対策① 値のバインド(プレースホルダ)

IP値を文字列連結せず、プレースホルダでデータとしてDBへ渡していた。入力がSQLの「命令」として解釈される余地がない → SQLiは構造的に不成立。フレームワークを使う大きな利点の一つ。

対策② DBの文字コード検証

ペイロードの不正バイト(overlong UTF-8 等)は、カラムの文字コードではそもそも保存できない並び。DBが「無効」と判断して書き込みを拒否し、例外を投げた → 攻撃は失敗し、しかも検知できた。

起きたのは「攻撃者自身のリクエストが500になり、例外通知が大量に飛んだ」だけ。漏えい・改ざん・不正ログインはいずれもなし。多層防御が想定どおり機能した好例です。

本当の問題:すべてのプロキシを信頼していた

ここで終われば問題なしですが、見過ごせない点があります。そもそも、なぜ偽XFFがIP判定まで届いたのか?

✗ 全プロキシ信頼(ワイルドカード)

クライアントの偽XFF → 「本物のIP」として採用 → ログ・セッション・IP判定に偽の値が入る

✓ 何も信頼しない(初期値)

偽XFFは無視 → 直接接続元の実IPを採用 → 詐称が効かない

信頼するプロキシをワイルドカードにすると、誰の偽XFFも「本物のIP」として通る。前段にプロキシが無いなら『何も信頼しない』が正解。

この構成はリバースプロキシもCDNも挟まないWebサーバー直結でした。つまりXFFを信頼する必要が無いのに「全部信頼」にしていた。「とりあえず全許可」は、開発時の仮の設定がそのまま本番に残るよくある例です。

見落としやすい点:XFF偽装の害はインジェクションだけではない

クライアントIPを信じる処理は、すべて偽の値で動いてしまいます。レート制限の回避・IP許可リストの突破・BAN回避・監査ログの偽装が起こりえます。インジェクション対策が十分でも、これらは別の問題として残ります。「IPを信じている場所」を棚卸しして、その信頼の根拠(=プロキシ設定)を確認してください。

対策:境界でサニタイズ → 信頼を適正化

1

境界でIPヘッダをサニタイズ(応急処置・すぐ効く)

プロキシ信頼の処理が走る前に、XFFを区切って各要素を「妥当なIPか」で検証し、妥当なIPだけ残す。1つも無ければヘッダごと削除。正規利用者の本物IPは検証を通るので影響なし、探索文字列は捨てられる。
2

信頼するプロキシを適正化(根本対策)

ワイルドカードをやめる。前段にプロキシが無いなら何も信頼しない。プロキシ信頼は http/https 判定にも関わるため、変更でリダイレクトループ等の副作用が出うる → まず1環境で先行検証。
3

“妥当なIP”を“本物のIP”と混同しない

サニタイズは形式を整える処理にすぎない。形が正しいIPでも詐称された値かもしれない。本物のIPの信頼は、あくまで②のプロキシ設定で担保する。

検証:直したら攻撃を再現して確かめる

「直したつもり」を「確かに止まった」に変えます。①対策を1環境にだけ適用 → ②同じ不正ヘッダを自分で送る → ③エラー件数が増えないことを確認 → ④正常アクセスも従来どおり動くことを確認 → ⑤問題なければ全環境へ。段階適用が事故を防ぎます。

続き:IPを遮断した後、User-Agent経由で攻撃された

サニタイズを入れて数十分後、また同じ種類のエラーが来ました。今度エラーになったのは別のカラムで、ip_address ではなく user_agent です。IPヘッダで拒否された攻撃者が、同じ不正ペイロードを User-Agent ヘッダに入れて送ってきたのです。ここで発想を変える必要があります。

入口:XFF・User-Agent・Referer・… (無数にある)
↓ このうち“保存される”のは
出口:sessions テーブルの ip_address と user_agent の2つだけ
↓ だから無害化すべき入口は
その2つに届くヘッダ(IP系 + User-Agent)だけでよい
危険なヘッダを1つずつ塞いでも、別のヘッダで同じことが起きる。信頼できない値が最終的にどこへ保存されるか(出口)から逆算すれば、対策すべきヘッダ(入口)が確定する。

正しい問いは「どのヘッダが危険か」ではなく「信頼できない値が、最終的にどのDBカラムに保存されるか」です。保存先が ip_address と user_agent の2つだけなら、その2つに届く入口(IP系ヘッダ+User-Agent、念のため Referer も)を無害化すれば、セッション由来のエラーは原理的に止まります。保存されない他のヘッダに移されても、同じエラーは起きません。正規の User-Agent は ASCII/正常なUTF-8 なので、無効バイトだけを落とす処理を足しても無影響です。

当サイトの視点:保存先(出口)から逆算し、ヘッダ(入口)を1つずつ塞がない

ヘッダを1個ずつ塞いでも、攻撃者が使えるヘッダは他にいくらでもあります。出口(保存先カラム・出力先)から逆算して、そこに至る入口を一度に全部洗い出すのが正解。IPヘッダを対策したら User-Agent を使われた今回の経験のとおり、応急処置でヘッダを塞ぐときは同じ保存先に届く他の入口も同時に塞ぐ。そして2回目も、再現して止まったことを確認してから展開しました。

さらに続き:保存する直前の処理で防いだ

User-Agent も塞ぎ「今度こそ」と思った直後、また ip_address カラムで同じエラーが来ました。入口のサニタイズはデプロイ済みで、攻撃と同じ不正ヘッダを自分で送るテストでは再現しない。なのに本番では再発する。ここがこの事例でいちばん重要な点です。

理由は、フレームワークの「クライアントIP判定」が内部で複雑だったこと。複数のヘッダ表記(X-Forwarded-For・Forwarded・独自ヘッダ)、プロキシ信頼設定との組み合わせ、パース順やキャッシュ……これらの内部経路のどこかで、こちらが無害化したつもりの値とは別の経路から、不正な値が再び「クライアントIP」として使われていました。入口の検査だけでは、すべては防げないのです。

✗ 入口で守る

リクエストのヘッダを浄化 → 内部の別経路で不正なクライアントIPが再び使われる → 防げない

✓ 出口で守る

DB保存の直前で検査 → 途中の経路にかかわらず必ず通る場所 → 防げる

入口(入ってくる値の検査)は、フレームワーク内部の別の処理経路で回避されうる。出口(DBに書く直前)はすべての値が必ず通る場所なので、回避されない。

そこで考え方を変え、出口(DBに書く一歩手前)で守ることにしました。セッション保存クラスを継承し、保存値を作る部分だけ差し替えます(概念):保存用IPは「妥当なIPでなければ null」、保存用User-Agentは「不正バイトを除去し長さを制限」。クライアントIP判定が内部でどう処理されても、DBに書く直前で必ず検査を通るので、回避されません。2つの攻撃経路で十数回ずつ同じ不正ヘッダを送ってもエラーは増えず、正規ユーザーのセッションも正常でした。

当サイトの視点:最後の防御策は、値を使う場所(出口)での検証

セキュリティの基本は「入力を信用せず、使う場所(出口)で必ず検証・エスケープする」ことです。入口の検査は早い段階で不正な値を拒否するために有効ですが、フレームワークが内部で値を解決・加工する種類の情報(クライアントIP判定など)は、入口の検査を経ずに再び現れることがあります。DB・HTML・コマンドなどへ渡す直前、つまり必ず通る場所で検証・エスケープするのが最後の防御策です。入口と出口は別物として両方やります。

教訓:不正な入力は、エラーとして目に見える形で拒否されるほうが安全

今回、攻撃は失敗したうえに大量のエラー通知という形で可視化されました。これは悪い結果ではありません。気づかれずに通過されるより、エラーとして拒否されて通知が来るほうが安全です。エラー通知を不要な通知として無視せず、異常に気づく手がかりとして活かしてください。そして「実害は防げた」で終わらせず、「なぜ内部まで届いたのか」を直すのが当サイトの立場です。

次に読む

よくある質問

QX-Forwarded-For は信用できる?
A

そのままでは信用できません。XFFはクライアントが自由に値を入れられるヘッダで、攻撃者は本物らしいIPにも、インジェクション探索の文字列にも詐称できます。信頼できるのは『自分が前段に置いた、信頼するプロキシが付けたXFF』だけです。

QリバースプロキシやCDNを使っていない場合はどうする?
A

XFFを一切信頼しない(フレームワークの初期値のまま)にします。前段にプロキシが無いのに『すべてのプロキシを信頼』する設定にすると、誰でも偽のIPをアプリに渡せます。構成が無いなら何も信頼しないのが正解です。

QIPヘッダをサニタイズすれば安全?
A

インジェクション探索や大量のエラーは止まりますが、それは応急処置です。『妥当なIP』に整形できても『本物のIP』である保証はありません。本物のIPの信頼は、あくまで信頼するプロキシの設定で担保します。

QIPヘッダを直したのに、また同じエラーが出ました。なぜ?
A

同じ不正ペイロードが別のヘッダ(例:User-Agentヘッダ)に移され、同じ保存先カラムに届いたためです。ヘッダを1つずつ塞いでも、別のヘッダで同じことが繰り返されます。『信頼できない値が最終的にどのDBカラム/出力先に届くか』から逆算し、そこに届くヘッダ(IP系+User-Agent等)をまとめて無害化してください。