Однажды утром, просматривая почту, обнаружил письмо от моего хостера о закрытии одного из моих проектов из-за перегрузки сервера. Естественно, я сразу же взялся за расследование причины этого неприятного явления. После непродолжительного анализа логов, было выяснено, что мой сайт задолбили тысячами запросов с одного и того же IP.
Казалось, что нет ничего проще - закрыть доступ этому IP через файл .htaccess, но где гарантия, что подобное не повторится в будущем? Да и развелось "специалистов" как грибов после дождя.
Покопавшись в интернете, я так и не смог найти ничего подходящего для моей ситуации. Может плохо искал, а может это моя извечная тяга к изобретению своего велосипеда? Свои рассуждения я начал следующим образом:
1. Автоматическая блокировка IP
Она должна срабатывать при превышении определенного лимита запросов. Тут все зависит от самого сайта и его тематики. На моем, человек вряд-ли сделает больше 100 запросов в сутки.
2. Принадлежность запросов
Определение автоматических запросов. Понятное дело, что нормальный человек при серфинге по сайту не сможет просмотреть 100 страниц в минуту.
3. Рейтинг запросов
Вот это и есть самое сложное. Ведь система расчета средней частоты запросов здесь не прокатит. Допустим, робот посылает запросы пачками каждый час, в результате, среднее время между запросами может оказаться больше, чем у нормального серфера. Итак, надо разработать алгоритм, который будет адекватно расчитывать коэффициент "плотности" запросов.
Рассмотрим запросы с одного IP в течение минуты по секундам:
Человек: 1,3,6,24,51 Робот: 1,1,2,3,4,4,5,6,7,8,22,23,23,51,52,53,55,55,56
Что же мы видим? Различны не только количество запросов в минуте, но и промежутки времени между запросами.
Поэтому, нам достаточно запомнить лишь: IP, время первого запроса, время последнего запроса и результат промежуточного вычисления рейтинга "плотности" запросов.
Тут мы плавно переходим от теории к практике и, на примере использования возможностей PHP и MySQL, сделаем рабочий макет защиты.
1. Создаем таблицу
Чтобы хранить сводную информацию о запросах, сделаем таблицу:
CREATE TABLE `abi_logip` (
`id` int(11) unsigned NOT NULL auto_increment,
`user_ip` varchar(15) NOT NULL,
`time_start` int(11) unsigned NOT NULL,
`time_last` int(11) unsigned NOT NULL,
`num_access` int(11) unsigned NOT NULL,
`rating` decimal(16,12) NOT NULL default '1',
PRIMARY KEY (`id`),
UNIQUE KEY `user_ip` (`user_ip`)
) ENGINE=MyISAM;
2. Пишем код блокировки
Находим в своем проекте файл, который вызывается самым первым, перед началом отдачи контента. Цель - блокировать IP при минимальной нагрузке на сервер. Если такой страницы нет, то придется ее сделать и заинклюдить во всех вызываемых файлах. Пишем код блокировки:
$USER_IP=(isset($_SERVER['REMOTE_ADDR']))?$_SERVER['REMOTE_ADDR']:'';
$BLOCKED_IP_ARRAY = array_map('rtrim', file('blocked.ip'));
if( in_array($USER_IP, $BLOCKED_IP_ARRAY, true))
{
unset($BLOCKED_IP_ARRAY);
header('HTTP/1.1 403 Forbidden');
exit;
};
?>
Перед началом проверки работоспособности кода, необходимо создать пустой файл 'blocked.ip' в папке расположения скрипта. После этого, добавьте свой IP в этот файл и попробуйте открыть проект. Если он не открывается, то все работает правильно.
3. Анализируем атаки
Находим в коде проекта место, где подключаемся к базе данных и, сразу после подключения, вставляем следующий код:
$QUERY_LOGIP = mysql_query("SELECT * FROM `abi_logip`
WHERE(`user_ip`='$USER_IP')");
$LOGIP = mysql_fetch_array($QUERY_LOGIP, MYSQL_ASSOC);
mysql_free_result($QUERY_LOGIP);
if( $LOGIP )
{
$USER_NUM_ACCESS = $LOGIP['num_access'] + 1;
$NUM_SECONDS = time() - $LOGIP['time_last'];
$CUR_RATING = log($NUM_SECONDS);
$USER_RATING = $LOGIP['rating'] + $CUR_RATING;
mysql_query("UPDATE `abi_logip` SET `time_last`=".time().",
`num_access`=$USER_NUM_ACCESS,`rating`=$USER_RATING
WHERE(`user_ip`='$USER_IP')");
$USER_RATING = $USER_RATING / $USER_NUM_ACCESS;
}
else
{
$USER_RATING = 1;
$USER_NUM_ACCESS = 1;
mysql_query("INSERT INTO `abi_logip`(`user_ip`,`time_start`,
`time_last`,`num_access`,`rating`)
VALUES('$USER_IP',".time().",".time().",1,1)");
};
if( $USER_NUM_ACCESS >= 50 && $USER_RATING < 0.8 )
{
$f = fopen('blocked.ip', 'a');
fwrite($f, $USER_IP."\n");
fclose($f);
};
?>
Внутри первого условия идет алгоритм вычисления рейтинга "плотности" запросов, основанный на логарифмической зависимости. Это позволяет не улучшать рейтинг автоматическим запросам, чередующих блочные запросы с одиночными. Второй условный блок занимается блокировкой IP при выполнении условия. Т.е. записывается IP-адрес в файл блокировки.
Переменная $USER_NUM_ACCESS содержит общее количество запросов, а переменная $USER_RATING - рейтинг "плотности" запросов. Все запросы с рейтингом ниже 1, можно считать подозрительными при достаточном количестве запросов. У меня это количество установлено равным 50, а пороговый рейтинг 0.8
Данный алгоритм не защитит от профессиональных атак, но поможет избежать излишней и неблагоприятной нагрузки на сервер. Чтобы обсудить это в форуме, нажмите здесь. |