PHP Manual
/
セキュリティ

PHP でユーザーの IP アドレスを取得する

28. 02. 2020

Obsah článku

PHPでは、基本的なレベルでIPアドレスを検出することは非常に簡単です。

echo 'あのね、あなたのIPアドレスは' . $_SERVER['REMOTE_ADDR'] . '?';

Warning: $_SERVER['REMOTE_ADDR'] フィールドのキーとしてIPアドレスを取得することは、PHPがブラウザから呼び出された場合のみ可能です。CLIモード(例えば、ターミナルからcronで実行)では、IPアドレスは利用できません(ネットワーク要求が行われないので、これは理にかなっています)。

信頼性の高いIPアドレス発見

何年もかけて開発した結果、最終的にこの実装にこだわりました。

function getIp(): string
{
if (isset($_SERVER['http_cf_connecting_ip'])) { // Cloudflare対応
$ip = $_SERVER['http_cf_connecting_ip'];
} elseif (isset($_SERVER['REMOTE_ADDR']) === true) {
$ip = $_SERVER['REMOTE_ADDR'];
if (preg_match('/^(?:127|10)\.0\.0\.[12]?\d{1,2}$/', $ip)) {
if (isset($_SERVER['HTTP_X_REAL_IP'])) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
} elseif (isset($_SERVER['http_x_forwarded_for'])) {
$ip = $_SERVER['http_x_forwarded_for'];
}
}
} else {
$ip = '127.0.0.1';
}
if (in_array($ip, ['::1', '0.0.0.0', 'ローカルホスト'], true)) {
$ip = '127.0.0.1';
}
$filter = filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4);
if ($filter === false) {
$ip = '127.0.0.1';
}
return $ip;
}

それなら、ずっといい。

echo 'あのね、あなたのIPアドレスは' . getIp() . '?';

IPが直接検出できる場合、IPv6のみである場合、CLIモード(cronなど)である場合は、 127.0.0.1 (localhost) を返します。

X-Forwarded-ForX-Real-IP` ヘッダを考慮した実装は、PHPで直接行うには非常に危険です。データが簡単に変更され、攻撃者が偽のIPアドレスを偽装して、例えば、サイトの管理画面を見たりデバッグモードを有効にしたりできるからです (Nette Tracy).一方、プロキシされたリクエストも受け入れなければならない(例えば、Cloudflareを経由してプロキシされたトラフィックや、同じマシン上でApacheとNgnixを動作させている場合、それらが互いに直後にローカルで呼び出された場合など)。

それは、Apache (RemoteIP 拡張) と Nginx (remote_ip 拡張) では X-Forwarded-For が訪問者の実際の IP アドレスから設定され、その IP アドレスが HTTP ヘッダーで設定できないことを確認することです。

$_SERVER['REMOTE_ADDR']` フィールドは自動的に正しい IP アドレス (つまり、リクエストが PHP に直接来たときの IP アドレス) を取得するので、それを扱う必要はありません。

プロキシ経由のユーザーアクセス

ユーザーがプロキシ経由でアクセスすることはよくあることです。そして、実際のIPアドレスが変数 $_SERVER['HTTP_X_FORWARDED_FOR'] に格納されます。

このケースは、例えば、サーバー上のルーティングが Ngnix -> Apache -> PHP という方法で解決され、NgnixApache の前にリバースプロキシとして機能する場合に発生する可能性があります。この場合、PHP は内部ネットワーク内の IP アドレス (通常は 127.0.0.* という形式) のみを認識します。

例えば、Cloudflareサービスがこのような動作をすることがあり、実際のユーザーのIPアドレスとプロキシのどちらを操作しているのかに注意する必要があります。私にとっては、この記事の冒頭で紹介した getIp() 関数を使うのが一番良い方法です。プロキシされたリクエストごとに自動的に送信される $_SERVER['HTTP_CF_CONNECTING_IP'] キーの存在を確認することで、Cloudflareの検出を確実にすることができます。

VPN/プロキシ検出

プロキシやVPNの利用を確実に検知することはできませんが、実際の環境では、少なくとも一部のトラフィックをフィルタリングすることができます。

その方法はいくつかあります。IPアドレスの範囲を取り、リクエストの来たIPアドレスを比較する。

VPNプロバイダによっては、IPアドレスのリストが非公式に公開されています(例えば、https://gist.github.com/JamoCA…)。Torの出口ノードの場合は、公式(https://blog.torproject.org/changes-tor-exit-list…)に公開されています。

また、どこかでオンラインリクエストを行うという方法もありますが、これはサービスが機能していない場合にページの読み込みを遅らせたり、訪問者のIPアドレスを第三者に「漏洩」させたりする可能性があります。2023年以降は、ユーザーデータの保護と操作に重点が置かれ始めるので、このアプローチには強く反対します。

このオンラインクエリは「素朴」であり、誰がその範囲を所有しているか、またはそれがプロキシ/VPNであるかどうかを確認する必要があるだけです(一部のサービスはこれを返すことがありますが、デフォルトでは、例えばwhoisサービスからの「IP情報」の一部にはなっていません)。

(ほとんどの場合、ある種の評価レーティングが使用され、あるIPアドレスは他のIPアドレスよりも「ゴミ」であるとされています。統計的には、自宅のIPアドレス(おそらく「感染した」自宅のIPアドレスを除く)よりも、さまざまなプロキシ、VPN、Torから来るゴミの方が多いのです。このような評判の評価は、いくつかのDNSブロックリストによって提供されており、いくつかのランダムなリスト、https://en.m.wikipedia.org/wiki/Comparison_of_DNS…

検出の目的が何であるかに大きく依存します。

IPアドレス記憶装置

どのようなIPアドレスが利用可能かによって異なります。

  • IPv4のIPアドレスは4バイトで格納することができ、そのために ip2long 関数が使用される。
  • しかし、IPv6のIPアドレスは16バイトを使わなければならず、変換機能はない。

もしデータベースサーバーがIPアドレスのデータ型を直接サポートしていない場合は、IPアドレスを varchar(39) として格納することをお勧めします。この場合、両方のバージョンが文字列として収まり、人間が読めるようになります。

IPアドレスを格納する場合、gethostbyaddr関数によって検出されたドメイン名も格納するのが理にかなっているかどうかを検討します。リストアップして検索しても名前がわからないのは、非常に時間がかかる上に、時間が経つと変わってしまうことがあるからです。

訪問者のIPアドレスをブロックする

理想的な解決策は、ブロックされたIPアドレスのリストを作成し、各リクエストでこのリストと現在のIPアドレスを比較することです。アドレスが一致した場合、リクエストは直ちに停止されます。

$blackList = [
'ファーストチップ',
'ドルハチップ',
];
if (\in_array(getIp(), $blackList, true) === true) {
echo '残念ながらあなたのIPアドレスはブロックされています :-(';
die; // リクエストを終了する
}

この例では、上記の例と同様に getIp() 関数を実装することを想定しています。

より強力な解決策は、インデックスが配列内に出現しているかどうかをチェックすることです。

$blackList = [
'ファーストチップ' => true,
'ドルハチップ' => true,
];
if (isset($blackList[getIp()]) === true) {
echo '残念ながらあなたのIPアドレスはブロックされています :-(';
die; // リクエストを終了する
}

サーバーのIPアドレスとサーバー名

サーバのIPアドレスは通常 $_SERVER['SERVER_ADDR'] フィールドに格納され、その名前は gethostbyaddr($_SERVER['SERVER_ADDR']) 構造体で取得することができる。

ただし、Ngnix -> Apache -> PHP というコンセプトで、Ngnix がリバースプロキシの役割をしている場合、サーバの本当のIPアドレスは表示されません。

この場合、サーバー名は $_SERVER['SERVER_NAME'] フィールドで確認するか、あるいは php_uname('n') 関数を使用して確認することができます。uname関数公式ドキュメント

そして、このトリックを使ってサーバーの公開IPアドレスを見つけることができます: gethostbyname(php_uname('n')).

Jan Barášek   Více o autorovi

Autor článku pracuje jako seniorní vývojář a software architekt v Praze. Navrhuje a spravuje velké webové aplikace, které znáte a používáte. Od roku 2009 nabral bohaté zkušenosti, které tímto webem předává dál.

Rád vám pomůžu:

Související články

1.
8.
Status:
All systems normal.
2024