X-Forwarded-For は HTTP 拡張ヘッダーです。HTTP/1.1(RFC 2616)プロトコルではその定義はありませんでした。もともとはキャッシュプロキシソフトウェアである Squid によって導入され、HTTP リクエスト元の実際の IP を表すために使われました。現在では事実上の標準となり、各 HTTP プロキシやロードバランサーなどの転送サービスで広く使われ、RFC 7239(Forwarded HTTP Extension)標準にも記載されています。
X-Forwarded-For リクエストヘッダーの形式は非常に単純で、次のようになります:
X-Forwarded-For: client, proxy1, proxy2
見てわかるように、XFF の内容は「カンマ + スペース」で区切られた複数の部分から構成され、最初にあるのはサーバーから最も遠いデバイスの IP、次に各段階のプロキシデバイスの IP です。
もし HTTP リクエストがサーバーに到達するまでに、3つのプロキシ Proxy1、Proxy2、Proxy3 を経由し、それぞれの IP が IP1、IP2、IP3、ユーザーの実際の IP が IP0 である場合、XFF 標準に従えば、サーバー側は最終的に以下の情報を受け取ります:
X-Forwarded-For: IP0, IP1, IP2
Proxy3 はサーバーに直接接続しており、XFF に IP2 を追加します。これは Proxy2 のリクエストを転送していることを示します。リストには IP3 は含まれていません。IP3 はサーバー側で Remote Address フィールドから取得できます。HTTP 接続は TCP 接続に基づいており、HTTP プロトコルには IP の概念はありません。Remote Address は TCP 接続に由来し、サーバーと TCP 接続を確立したデバイスの IP を表します。この例では IP3 になります。
Remote Address は偽造できません。TCP 接続を確立するには3ウェイハンドシェイクが必要であり、送信元 IP を偽造した場合、TCP 接続を確立できないため、その後の HTTP リクエストも発生しません。言語によって Remote Address の取得方法は異なります。例えば PHP では $_SERVER["REMOTE_ADDR"]、Node.js では req.connection.remoteAddress ですが、原理は同じです。
問題
上記の背景知識を踏まえて、問題について説明します。テスト用に Node.js で最もシンプルな Web サーバーを書きました。HTTP プロトコルは言語に依存しないため、ここで Node.js を使うのはデモのためだけであり、他のどの言語に置き換えても同じ結論になります。また、この記事で Nginx を使うのも同じ理由で、興味があれば Apache や他の Web サーバーに置き換えても同様です。
次のコードは 9009 番ポートをリッスンし、HTTP リクエストを受け取った後にいくつかの情報を出力します:
var http = require('http');
http.createServer(function (req, res) {
res.writeHead(200, {'Content-Type': 'text/plain'});
res.write('remoteAddress: ' + req.connection.remoteAddress + '\n');
res.write('x-forwarded-for: ' + req.headers['x-forwarded-for'] + '\n');
res.write('x-real-ip: ' + req.headers['x-real-ip'] + '\n');
res.end();
}).listen(9009, '0.0.0.0');
このコードには、前述の Remote Address と X-Forwarded-For に加えて、X-Real-IP もあります。これもカスタムヘッダーフィールドです。X-Real-IP は通常、HTTP プロキシがそれと TCP 接続を確立したデバイスの IP を示すために使われます。このデバイスは他のプロキシの場合もあれば、実際のリクエスト元の場合もあります。注意すべき点として、X-Real-IP は現在どの標準にも属しておらず、プロキシと Web アプリケーションの間で任意のカスタムヘッダーを使ってこの情報を伝達する取り決めができます。
それでは、ドメイン名 + ポート番号でこの Node.js サービスに直接アクセスし、さらに Nginx リバースプロキシを設定します:
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://127.0.0.1:9009/;
proxy_redirect off;
}
私の Nginx は 80 番ポートをリッスンしているため、ポート番号なしで Nginx が転送したサービスにアクセスできます。
Node サービスへの直接アクセスをテスト:
curl http://t1.imququ.com:9009/ remoteAddress: 114.248.238.236 x-forwarded-for: undefined x-real-ip: undefined
私のコンピュータは Node.js サービスに直接接続しているため、Remote Address は私の IP になります。同時に追加のカスタムヘッダーを指定していないので、後ろの2つのフィールドはどちらも undefined です。
次に、Nginx が転送したサービスにアクセスします:
curl http://t1.imququ.com/ remoteAddress: 127.0.0.1 x-forwarded-for: 114.248.238.236 x-real-ip: 114.248.238.236
今回は、私のコンピュータは Nginx を経由して Node.js サービスにアクセスするため、取得される Remote Address は実際には Nginx のローカル IP です。そして前述の Nginx 設定内のこの2行が機能し、リクエストに2つのカスタムヘッダーが追加されました:
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
実際、本番環境で Web アプリケーションをデプロイする場合、通常は上記の2番目の方法を採用します。多くの利点があります。しかし、これにより1つの落とし穴が生じます:多くの Web アプリケーションはユーザーの本当の IP を取得するために、HTTP リクエストヘッダーから IP を取得します。
HTTP リクエストヘッダーは自由に作成できます。curl の -H パラメータを使って X-Forwarded-For と X-Real-IP を作成し、もう一度テストしてみます。
Node.js サービスに直接アクセス:
curl http://t1.imququ.com:9009/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-IP: 2.2.2.2' remoteAddress: 114.248.238.236 x-forwarded-for: 1.1.1.1 x-real-ip: 2.2.2.2
Web アプリケーションにとって、X-Forwarded-For と X-Real-IP は単なる2つの通常のリクエストヘッダーに過ぎないため、当然何も処理されずそのまま出力されます。これは、直接接続のデプロイ方式では、TCP 接続から得られる Remote Address 以外に、リクエストヘッダーに含まれる IP 情報はすべて信頼できないことを示しています。
Nginx が転送したサービスにアクセス:
curl http://t1.imququ.com/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-IP: 2.2.2.2' remoteAddress: 127.0.0.1 x-forwarded-for: 1.1.1.1, 114.248.238.236 x-real-ip: 114.248.238.236
今回は、Nginx が X-Forwarded-For の後ろに私の IP を追加し、X-Real-IP リクエストヘッダーを私の IP で上書きします。つまり、Nginx による加工があれば、X-Forwarded-For の最後の部分と X-Real-IP 全体の内容は偽造できず、ユーザー IP の取得に使用できます。
ユーザー IP は、多くの場合、Web セキュリティに関連するシナリオで使用されます。例えば、ユーザーのログイン地域の確認、IP ベースのアクセス頻度制御などです。このようなシナリオでは、IP が偽造できないことを確保することがより重要です。前述のテストと分析により、ユーザーに直接公開してデプロイされた Web アプリケーションでは、TCP 接続から得られる Remote Address を使用する必要があります。Nginx などのリバースプロキシをデプロイした Web アプリケーションでは、Set Header の動作を正しく設定した上で、Nginx から渡される X-Real-IP または X-Forwarded-For の最後の部分を使用できます(実際にはこれらは必ず等価です)。
では、Web アプリケーション自体は、リクエストが直接来たのか、それとも制御可能なプロキシによって転送されたのかをどのように判断するのでしょうか?プロキシ転送時に追加のリクエストヘッダーを追加する方法もありますが、あまり安全ではありません。リクエストヘッダーはあまりにも簡単に偽造できるからです。どうしてもこの方法を使う場合は、このカスタムヘッダーは十分に長くて珍しいものである必要があり、さらに漏洩しないように厳重に管理する必要があります。
Remote Address がローカル IP かどうかを判断する方法もありますが、これも完全ではありません。Nginx が稼働しているサーバー上からアクセスする場合、直接接続でも Nginx プロキシ経由でも、Remote Address は 127.0.0.1 になるからです。この問題は通常無視できる場合が多いですが、より厄介なのは、リバースプロキシサーバーと実際の Web アプリケーションが必ずしも同じサーバーにデプロイされているとは限らないことです。そのため、より合理的な方法は、すべてのプロキシサーバーの IP リストを収集し、Web アプリケーションが Remote Address を取得した後に逐一照合して、どの方法でアクセスされたかを判断することです。
通常、ロジックを簡素化するために、本番環境ではポート番号を付けて Web アプリケーションに直接アクセスする方法を遮断し、Nginx 経由でのみアクセスできるようにします。では、これで問題はないのでしょうか?そうとは限りません。
まず、ユーザーが本当にプロキシを経由して Nginx にアクセスする場合、X-Forwarded-For の最後の部分と X-Real-IP で得られるのはプロキシの IP です。セキュリティ関連のシナリオではこれしか使えませんが、IP に基づいて現在地の天気を表示するなどの一部のシナリオでは、できるだけユーザーの実際の IP を取得する必要があります。このような場合、X-Forwarded-For の最初の IP が役立ちます。ここで注意すべき問題が1つあります。先ほどの例を使ってテストします:
curl http://t1.imququ.com/ -H 'X-Forwarded-For: unknown, <>"1.1.1.1' remoteAddress: 127.0.0.1 x-forwarded-for: unknown, <>"1.1.1.1, 114.248.238.236 x-real-ip: 114.248.238.236
X-Forwarded-For の最後の部分は Nginx によって追加されましたが、それ以前の部分はすべて Nginx が受信したリクエストヘッダーに由来します。この部分のユーザー入力内容は完全に信頼できません。使用時には特に注意が必要で、IP 形式に一致する場合のみ使用できます。そうしないと、SQL インジェクションや XSS などのセキュリティ脆弱性を引き起こしやすくなります。
結論
- 外部に直接サービスを提供する Web アプリケーションは、セキュリティ関連の操作を行う場合、Remote Address を介してのみ IP を取得でき、いかなるリクエストヘッダーも信頼してはなりません。
- Nginx などの Web サーバーでリバースプロキシを行う Web アプリケーションは、設定が正しいことを前提として、次を使用する必要があります:
X-Forwarded-For最後の部分、またはX-Real-IPを使用して IP を取得します(Remote Address から得られるのは Nginx が稼働するサーバーの内部ネットワークの IP であるため)。同時に、Web アプリケーションが外部に直接サービスを提供することも禁止すべきです。 - セキュリティに関係のないシナリオ、例えば IP から所在地の天気を表示する場合などでは、次から IP を取得できます:
X-Forwarded-For前の方の位置から IP を取得できますが、IP 形式の正当性を検証する必要があります。
PS:ネット上の一部の記事では、このように Nginx を設定することを推奨していますが、実際には合理的ではありません:
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr;
このように設定すると、確かに安全性は向上しますが、リクエストが Nginx に到達する前のすべてのプロキシ情報が消去され、本当にプロキシを使用しているユーザーにより良いサービスを提供できなくなります。やはりこの仕組みの原理を理解し、具体的なシナリオに応じて具体的に分析すべきです。
原文アドレス:https://imququ.com/post/x-forwarded-for-header-in-http.html