PHP-скрипт для защиты от DDOS, парсинга и ботов

от автора

Нижепредложенный скрипт был наконец написан, когда устал каждый день смотреть логи сервера, где чётко видно было «мусорные» запросы, раздувающие лог и дающие лишнюю нагрузку на хостинге.

Огромная часть бот-трафика это запросы вида:

Hidden text
  • 51.120.240.89 — — [01/Apr/2022:15:28:26 +0300] «GET /wp-content/plugins/ubh/up.php/.well-known/ HTTP/1.1» 403 «Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36»

  • 51.120.240.89 — — [01/Apr/2022:15:28:44 +0300] «GET /wp-content/plugins/ubh/up.php/.well-known/ HTTP/1.1» 403 «Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36»

  • 51.120.240.89 — — [01/Apr/2022:15:29:16 +0300] «GET /wp-content/uploads/ HTTP/1.1» 403 «Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36»

  • 51.120.240.89 — — [01/Apr/2022:15:29:42 +0300] «GET /wp-includes/ HTTP/1.1» 403 «Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0

И все 50-200 строк в таком духе, за короткий промежуток времени.

То есть видим ip-адрес атакующего — [дата:время] «вид запроса и собственно сам запрос (/wp-content/wp-includes)» код ответа сервера 403 (т.к. ip-адрес не российский, город Осло, но об этом в следующих постах) строка UserAgent (может быть любой).

Сам скрипт настраиваемый по частоте запросов в единицу времени. Например, 4 запроса за 1 секунду приведут к блокировке атакующего ip-адреса на 60 секунд.

Чтобы не было вопросов «А как же поисковые боты, типа Яндекса и пр.» встроил проверку на поискового бота. Если это например робот Яндекса, то скрипт пропускает его и не проверяет больше ничего, не следит за активностью. Если это не из списка разрешённых ботов, то идёт отслеживание активности и если это откровенно «долбёжка», парсинг или как в вышеприведённом кусочке лога — попытка узнать/взломать вашу CMS — однозначно блокировка на указанное в настройках время (у меня 60 сек).

Собственно сам скрипт:

/*** Класс проверки и блокировки ip-адреса. */ class BotBlockIp {     /*** Время блокировки в секундах. */     const blockSeconds = 60;     /**      * Интервал времени запросов страниц.      */     const intervalSeconds = 1;     /**      * Количество запросов страницы в интервал времени.      */     const intervalTimes = 4;     /**      * Флаг подключения всегда активных пользователей.      */     const isAlwaysActive = true;     /**      * Флаг подключения всегда заблокированных пользователей.      */     const isAlwaysBlock = true;     /**      * Путь к директории кэширования активных пользователей.      */     const pathActive = 'active';     /**      * Путь к директории кэширования заблокированных пользователей.      */     const pathBlock = 'block';     /**      * Флаг абсолютных путей к директориям.      */     const pathIsAbsolute = false;     /**      * Список всегда активных пользователей.      */     public static $alwaysActive = array(       );      /**      * Список всегда заблокированных пользователей.      */     public static $alwaysBlock = array(       );      /**      * Метод проверки ip-адреса на активность и блокировку.      */     public static function checkIp() {  // Если это поисковый бот, то выходим ничего не делая if(self::is_bot()){ return; }          // Получение ip-адреса         $ip_address = self::_getIp();          // Пропускаем всегда активных пользователей         if (in_array($ip_address, self::$alwaysActive) && self::isAlwaysActive) {             return;         }          // Блокируем всегда заблокированных пользователей         if (in_array($ip_address, self::$alwaysBlock) && self::isAlwaysBlock) {     header('HTTP/1.0 403 Forbidden');             echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';             echo '<html xmlns="http://www.w3.org/1999/xhtml">';             echo '<head>';             echo '<title>Вы заблокированы</title>';             echo '<meta http-equiv="content-type" content="text/html; charset=utf-8" />';             echo '</head>';             echo '<body>';             echo '<p style="background:#ccc;border:solid 1px #aaa;margin:30px au-to;padding:20px;text-align:center;width:700px">';             echo 'Вы заблокированы администрацией ресурса.<br />';             exit;         }          // Установка путей к директориям         $path_active = self::pathActive;         $path_block = self::pathBlock;          // Приведение путей к директориям к абсолютному виду         if (!self::pathIsAbsolute) {             $path_active = str_replace('\\' , '/', dirname(__FILE__) . '/' . $path_active . '/');             $path_block = str_replace('\\' , '/', dirname(__FILE__) . '/' . $path_block . '/');         }          // Проверка возможности записи в директории         if (!is_writable($path_active)) {             die('Директория кэширования активных пользователей не создана или закрыта для записи.');         }         if (!is_writable($path_block)) {             die('Директория кэширования заблокированных пользователей не создана или закрыта для записи.');         }          // Проверка активных ip-адресов         $is_active = false;         if ($dir = opendir($path_active)) {             while (false !== ($filename = readdir($dir))) {                 // Выбирается ip + время активации этого ip                 if (preg_match('#^(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})_(\d+)$#', $filename, $matches)) {                     if ($matches[2] >= time() - self::intervalSeconds) {                         if ($matches[1] == $ip_address) {                             $times = intval(trim(file_get_contents($path_active . $filename)));                             if ($times >= self::intervalTimes - 1) {                                 touch($path_block . $filename);                                 unlink($path_active . $filename);                             } else {                                 file_put_contents($path_active . $filename, $times + 1);                             }                             $is_active = true;                         }                     } else {                         unlink($path_active . $filename);                     }                 }             }             closedir($dir);         }          // Проверка заблокированных ip-адресов         $is_block = false;         if ($dir = opendir($path_block)) {             while (false !== ($filename = readdir($dir))) {                 // Выбирается ip + время блокировки этого ip                 if (preg_match('#^(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})_(\d+)$#', $filename, $matches)) {                     if ($matches[2] >= time() - self::blockSeconds) {                         if ($matches[1] == $ip_address) {                             $is_block = true;                             $time_block = $matches[2] - (time() - self::blockSeconds) + 1;                         }                     } else {                         unlink($path_block . $filename);                     }                 }             }             closedir($dir);         }          // ip-адрес заблокирован         if ($is_block) {             header('HTTP/1.0 502 Bad Gateway');             echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';             echo '<html xmlns="http://www.w3.org/1999/xhtml">';             echo '<head>';             echo '<title>502 Bad Gateway</title>';             echo '<meta http-equiv="content-type" content="text/html; charset=utf-8" />';             echo '</head>';             echo '<body>';             echo '<h1 style="text-align:center">502 Bad Gateway</h1>';             echo '<p style="background:#ccc;border:solid 1px #aaa;margin:30px au-to;padding:20px;text-align:center;width:700px">';             echo 'К сожалению, Вы временно заблокированы, из-за частого запроса страниц сайта.<br />';             echo 'Вам придется подождать. Через ' . $time_block . ' секунд(ы) Вы будете автоматически разблокированы.';             echo '</p>';             echo '</body>';             echo '</html>';             exit;         }          // Создание идентификатора активного ip-адреса         if (!$is_active) {             touch($path_active . $ip_address . '_' . time());         }     }      /**     * Метод получения текущего ip-адреса из переменных сервера.     */     private static function _getIp() {          // ip-адрес по умолчанию         $ip_address = '127.0.0.1';          // Массив возможных ip-адресов         $addrs = array();          // Сбор данных возможных ip-адресов         if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {             // Проверяется массив ip-клиента установленных прозрачными прокси-серверами             foreach (array_reverse(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])) as $value) {                 $value = trim($value);                 // Собирается ip-клиента                 if (preg_match('#^\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}$#', $value)) {                     $addrs[] = $value;                 }             }         }         // Собирается ip-клиента         if (isset($_SERVER['HTTP_CLIENT_IP'])) {             $addrs[] = $_SERVER['HTTP_CLIENT_IP'];         }         // Собирается ip-клиента         if (isset($_SERVER['HTTP_X_CLUSTER_CLIENT_IP'])) {             $addrs[] = $_SERVER['HTTP_X_CLUSTER_CLIENT_IP'];         }         // Собирается ip-клиента         if (isset($_SERVER['HTTP_PROXY_USER'])) {             $addrs[] = $_SERVER['HTTP_PROXY_USER'];         }         // Собирается ip-клиента         if (isset($_SERVER['REMOTE_ADDR'])) {             $addrs[] = $_SERVER['REMOTE_ADDR'];         }          // Фильтрация возможных ip-адресов, для выявление нужного         foreach ($addrs as $value) {             // Выбирается ip-клиента             if (preg_match('#^(\d{1,3}).(\d{1,3}).(\d{1,3}).(\d{1,3})$#', $value, $matches)) {                 $value = $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4];                 if ('...' != $value) {                     $ip_address = $value;                     break;                 }             }         }          // Возврат полученного ip-адреса         return $ip_address;     }      /**     * Метод проверки на поискового бота.     */     private static function is_bot()     { if (!empty($_SERVER['HTTP_USER_AGENT'])) { $options = array( 'YandexBot', 'YandexAccessibilityBot', 'YandexMobileBot','YandexDirectDyn', 'YandexScreenshotBot', 'YandexImages', 'YandexVideo', 'YandexVideoParser', 'YandexMedia', 'YandexBlogs', 'YandexFavicons', 'YandexWebmaster', 'YandexPagechecker', 'YandexImageResizer','YandexAdNet', 'YandexDirect', 'YaDirectFetcher', 'YandexCalendar', 'YandexSitelinks', 'YandexMetrika', 'YandexNews', 'YandexNewslinks', 'YandexCatalog', 'YandexAntivirus', 'YandexMarket', 'YandexVertis', 'YandexForDomain', 'YandexSpravBot', 'YandexSearchShop', 'YandexMedianaBot', 'YandexOntoDB', 'YandexOntoDBAPI', 'Googlebot', 'Googlebot-Image', 'Mediapartners-Google', 'AdsBot-Google', 'Mail.RU_Bot', 'bingbot', 'Accoona', 'ia_archiver', 'Ask Jeeves',  'OmniExplorer_Bot', 'W3C_Validator', 'WebAlta', 'YahooFeedSeeker', 'Yahoo!', 'Ezooms', '', 'Tourlentabot', 'MJ12bot', 'AhrefsBot', 'SearchBot', 'SiteStatus',  'Nigma.ru', 'Baiduspider', 'Statsbot', 'SISTRIX', 'AcoonBot', 'findlinks',  'proximic', 'OpenindexSpider','statdom.ru', 'Exabot', 'Spider', 'SeznamBot',  'oBot', 'C-T bot', 'Updownerbot', 'Snoopy', 'heritrix', 'Yeti', 'DomainVader', 'DCPbot', 'PaperLiBot' );   foreach($options as $row) { if (stripos($_SERVER['HTTP_USER_AGENT'], $row) !== false) { return true; } } }   return false; }  }  // Проверка текущего ip-адреса BotBlockIp::checkIp();

Для установки скрипта:

  • создаём папку, например block;

  • в ней создаём папки active и block;

  • создаём php-файл с вышеприведённым скриптом, например bot_block_ip.php;

  • на любом сайте, в индексном файле, в самом начале подключаем наш скрипт:

    Например, в Битрикс я разместил скрипт в папке tools:

<?include($_SERVER["DOCUMENT_ROOT"]."/bitrix/tools/block/bot_block_ip.php");?>

Скрипт прекрасно работает на php 7.4. Анализируя логи сервера стал замечать, что атакующие боты стали делать паузы между запросами (раньше доходило до 10-20 запросов в секунду, сейчас некоторые боты стали делать 1-2 запроса в секунду-две) и было решено — отсечь трафик не из России. Конечно пользоваться vpn и proxy никто не запрещает, но доля «мусорного» трафика сошла почти на нет. Если этот пост заинтересует достаточное количество людей, то напишу в следующих постах о доработанной версии этого скрипта, который блокирует все запросы не из России например (можно любую страну выбрать).

Спасибо, что прочитали. Сильно не критикуйте, скрипт работает, что от него и требовалось 🙂


ссылка на оригинал статьи https://habr.com/ru/post/659811/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *