PHP Manual
/
Безпека

Отримання IP-адреси користувача в PHP

28. 02. 2020

Obsah článku

У PHP дуже легко визначити IP-адресу на базовому рівні:

echo 'Ви знаєте, що ваша IP-адреса' . $_SERVER['REMOTE_ADDR'] . '?';

Попередження: Отримання IP-адреси як ключа поля $_SERVER['REMOTE_ADDR'] можливе лише у випадку, якщо 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', 'localhost'], 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-For і X-Real-IP, вкрай небезпечні безпосередньо в PHP, оскільки дані можуть бути легко змінені і зловмисник може підмінити фальшиву IP-адресу, щоб, наприклад, переглянути адміністрацію або активувати режим налагодження сайту (Nette Tracy). З іншого боку, нам доводиться приймати деякі проксі-запити (наприклад, при проксі трафіку через Cloudflare, або при запуску Apache і Ngnix на одній машині, коли вони викликаються локально один за одним).

У випадку прямого доступу користувача до сервера є лише одне правильне рішення - переконатися, що в Apache (через розширення RemoteIP) і в Nginx через розширення remote_ip, що X-Forwarded-For встановлюється з фактичної IP-адреси відвідувача, і що IP-адреса не може бути встановлена за допомогою HTTP-заголовка.

Поле $_SERVER['REMOTE_ADDR'] автоматично отримує правильну IP-адресу (тобто IP-адресу, з якої запит прийшов безпосередньо до PHP), і нам не потрібно з ним працювати.

Доступ користувачів через проксі

Часто трапляється, що користувач отримує доступ через проксі. Потім фактична IP-адреса зберігається у змінній $_SERVER['HTTP_X_FORWARDED_FOR'].

Такий випадок може виникнути, наприклад, коли маршрутизація на сервері вирішується методом Ngnix -> Apache -> PHP, де Ngnix служить зворотним проксі перед Apache. У цьому випадку PHP бачить тільки IP-адресу у внутрішній мережі (зазвичай виду 127.0.0.*).

Наприклад, сервіс Cloudflare може поводитися таким чином, тому слід звернути увагу на те, чи працюємо ми з IP-адресою реального користувача, чи з проксі-сервером. Для мене найкращим способом є використання функції getIp(), згаданої на початку цієї статті. Ми можемо забезпечити виявлення Cloudflare, перевіривши наявність ключа $_SERVER['HTTP_CF_CONNECTING_IP'], який автоматично передається в кожному проксі-запиті.

Виявлення VPN / проксі-серверів

Надійного виявлення використання проксі або VPN не існує, але в реальному середовищі ми можемо відфільтрувати принаймні частину трафіку.

Є кілька способів зробити це: Візьміть діапазон IP-адрес і порівняйте IP-адресу, з якої надійшов запит.

У деяких VPN-провайдерів списки IP-адрес доступні неофіційно (див., наприклад, https://gist.github.com/JamoCA…), у випадку вихідних вузлів Tor - офіційно (https://blog.torproject.org/changes-tor-exit-list-service, але мостів Tor там немає).

Інший варіант - зробити кудись онлайн-запит, що може як затримати завантаження сторінки, якщо сервіс не працює, так і "злити" IP-адреси відвідувачів третій стороні. Починаючи з 2023 року, я б наполегливо рекомендував відмовитися від такого підходу, оскільки він починає більше стосуватися захисту та маніпулювання даними користувачів.

Цей онлайн-запит може бути "наївним", і потрібно лише побачити, кому належить діапазон або чи це проксі/ VPN (деякі сервіси можуть повертати цю інформацію, але за замовчуванням вона не є частиною "IP-інформації", наприклад, від сервісу whois).

(Найчастіше) використовується якийсь рейтинг репутації, де деякі IP-адреси "смітять" більше, ніж інші. За статистикою, з різних проксі-серверів, VPN і Tor приходить більше лайна, ніж з домашніх IP-адрес (за винятком, можливо, "заражених" домашніх IP-адрес). Таку оцінку репутації пропонують деякі списки блокування DNS (див. випадковий список, https://en.m.wikipedia.org/wiki/Comparison_of_DNS… і колонку "Мета включення до списку"), або її надають безпосередньо компанії на кшталт Cloudflare у вигляді "управління ботами" тощо.

Багато залежить від того, яка мета виявлення.

Зберігання IP-адрес

Це залежить від того, яку IP-адресу ви маєте.

  • IPv4 IP-адреса може зберігатися в 4 байтах, для цього використовується функція ip2long,
  • Однак для IP-адреси IPv6 ми повинні використовувати 16 байт, а функції перетворення немає.

Якщо ваш сервер баз даних безпосередньо не підтримує тип даних для IP-адреси, я рекомендую зберігати IP-адресу у вигляді varchar(39), де обидві версії помістяться у вигляді рядка і будуть читабельними для людини.

Зберігаючи IP-адресу, подумайте, чи є сенс зберігати також доменне ім'я, визначене функцією gethostbyaddr. Ви не можете дізнатися імена при складанні списку та пошуку, тому що це займає дуже багато часу, і вони можуть змінюватися з часом.

Блокування IP-адреси відвідувача

Ідеальним рішенням є створення списку заблокованих IP-адрес і порівняння цього списку з поточною IP-адресою при кожному запиті. Якщо адреси збігаються, запит буде негайно зупинено.

$blackList = [
'first-ip',
'druha-ip',
];
if (\in_array(getIp(), $blackList, true) === true) {
echo 'На жаль, ваша IP-адреса заблокована :-(';
die; // Запит на вихід
}

У прикладі передбачається реалізація функції getIp() як у прикладі вище.

Більш потужним рішенням є перевірка на входження індексу в масив:

$blackList = [
'first-ip' => true,
'druha-ip' => 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