Обчислюємо потенційних «злих» ботів і блокуємо їх по IP

Обчислюємо потенційних «злих» ботів і блокуємо їх по IP

Доброго дня! У статті розповім як можна користувачам звичайного хостингу відловити IP адреси, що генерують зайве навантаження на сайт і потім блокувати їх за допомогою засобів хостингу, буде трохи php коду, кілька скріншотів.

Вхідні дані:

  1. Сайт створений на CMS WordPress
  2. Хостинг Бегет (це не реклама, але скрини адмінки будуть саме цього хостинг-провайдера)
  3. WordPress сайт запущений десь на початку 2000 і має велику кількість статей та матеріалів
  4. Версія PHP 7.2
  5. WP має останню версію
  6. З деяких пір сайт почав генерувати високе навантаження на MySQL за даними хостингу. Щодня це значення перевищувало 120% від норми на обліковий запис
  7. За даними Яндекс. Метрика сайт відвідує 100-200 осіб на добу

Насамперед було зроблено:

  1. Очищені таблиці БД від сміття, що накопичилося.
  2. Відключено непотрібні плагіни, прибрано ділянки застарілого коду

При цьому звертаю увагу, пробувалися варіанти кешування (плагіни кешування), проводилися спостереження - але навантаження в 120% від одного сайту було незмінним і могло тільки зростати.

То як виглядало зразкове навантаження за базами даних хостингу

Обчислюємо потенційних «злих» ботів і блокуємо їх по IP
У топі знаходиться сайт про який йде мова, трохи нижче за інші сайти які мають ту ж cms і приблизно таку ж відвідуваність, але створюють менше навантаження.

Аналіз

  • Було багато спроб з варіантами кешування даних, проводилися спостереження протягом декількох тижнів (благо хостинг за цей час мені жодного разу не написав що я такий поганий і мене відключать)
  • Був аналіз та пошук повільних запитів, потім була трохи змінена структура БД та тип таблиць
  • Для аналізу в першу чергу використовувався вбудований AWStats (він до речі допоміг обчислити найзлішу IP адресу за обсягом трафіку
  • Метрика – метрика дає інформацію лише про людей, а не про боти
  • Були спроби використовувати плагіни для WP, які вміють фільтрувати та блокувати відвідувачів навіть по країні знаходження та за різними комбінаціями
  • Зовсім радикальний спосіб виявився закрити сайт на добу з позначкою «Ми на технічному обслуговуванні» - це було зроблено за допомогою знаменитого плагіна. У цьому випадку навантаження очікуємо впало, але не до 0-лівих значень, так як ідеологія WP базується на хуках і плагіни починають свою активність при настанні якого-небудь «хука», а до настання «хука» можуть бути зроблені запити до БД

Ідея

  1. Обчислити IP адреси, які роблять багато запитів за короткий проміжок часу.
  2. Зафіксувати кількість звернень до сайту
  3. На основі кількості звернень блокувати доступ до сайту
  4. Блокувати за допомогою запису "Deny from" у файлі .htaccess
  5. Інші варіанти, на кшталт iptables та правил для Nginx не розглядаючи, бо пишу про хостинг

З'явилася ідея, отже, треба реалізовувати, як без цього…

  • Створюємо таблиці для накопичення даних
    CREATE TABLE `wp_visiters_bot` (
    	`id` INT(11) NOT NULL AUTO_INCREMENT,
    	`ip` VARCHAR(300) NULL DEFAULT NULL,
    	`browser` VARCHAR(500) NULL DEFAULT NULL,
    	`cnt` INT(11) NULL DEFAULT NULL,
    	`request` TEXT NULL,
    	`input` TEXT NULL,
    	`data_update` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    	PRIMARY KEY (`id`),
    	UNIQUE INDEX `ip` (`ip`)
    )
    COMMENT='Кандидаты для блокировки'
    COLLATE='utf8_general_ci'
    ENGINE=InnoDB
    AUTO_INCREMENT=1;
    

    CREATE TABLE `wp_visiters_bot_blocked` (
    	`id` INT(11) NOT NULL AUTO_INCREMENT,
    	`ip` VARCHAR(300) NOT NULL,
    	`data_update` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    	PRIMARY KEY (`id`),
    	UNIQUE INDEX `ip` (`ip`)
    )
    COMMENT='Список уже заблокированных'
    COLLATE='utf8_general_ci'
    ENGINE=InnoDB
    AUTO_INCREMENT=59;
    

    CREATE TABLE `wp_visiters_bot_history` (
    	`id` INT(11) NOT NULL AUTO_INCREMENT,
    	`ip` VARCHAR(300) NULL DEFAULT NULL,
    	`browser` VARCHAR(500) NULL DEFAULT NULL,
    	`cnt` INT(11) NULL DEFAULT NULL,
    	`data_update` DATETIME NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    	`data_add` DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
    	PRIMARY KEY (`id`),
    	UNIQUE INDEX `ip` (`ip`)
    )
    COMMENT='История всех запросов для дебага'
    COLLATE='utf8_general_ci'
    ENGINE=InnoDB
    AUTO_INCREMENT=1;
    
  • Створимо файл, у який помістимо код. Код буде запис до таблиць кандидатів на блокування та вести історію для дебага.

    Код файлу для запису IP адрес

    <?php
    
    if (!defined('ABSPATH')) {
        return;
    }
    
    global $wpdb;
    
    /**
     * Вернёт конкретный IP адрес посетителя
     * @return boolean
     */
    function coderun_get_user_ip() {
    
        $client_ip = '';
    
        $address_headers = array(
            'HTTP_CLIENT_IP',
            'HTTP_X_FORWARDED_FOR',
            'HTTP_X_FORWARDED',
            'HTTP_X_CLUSTER_CLIENT_IP',
            'HTTP_FORWARDED_FOR',
            'HTTP_FORWARDED',
            'REMOTE_ADDR',
        );
    
        foreach ($address_headers as $header) {
            if (array_key_exists($header, $_SERVER)) {
    
                $address_chain = explode(',', $_SERVER[$header]);
                $client_ip = trim($address_chain[0]);
    
                break;
            }
        }
    
        if (!$client_ip) {
            return '';
        }
    
    
        if ('0.0.0.0' === $client_ip || '::' === $client_ip || $client_ip == 'unknown') {
            return '';
        }
    
        return $client_ip;
    }
    
    $ip = esc_sql(coderun_get_user_ip()); // IP адрес посетителя
    
    if (empty($ip)) {// Нет IP, ну и идите лесом...
        header('Content-type: application/json;');
        die('Big big bolt....');
    }
    
    $browser = esc_sql($_SERVER['HTTP_USER_AGENT']); //Данные для анализа браузера
    
    $request = esc_sql(wp_json_encode($_REQUEST)); //Последний запрос который был к сайту
    
    $input = esc_sql(file_get_contents('php://input')); //Тело запроса, если было
    
    $cnt = 1;
    
    //Запрос в основную таблицу с временными кондидатами на блокировку
    $query = <<<EOT
        INSERT INTO wp_visiters_bot (`ip`,`browser`,`cnt`,`request`,`input`)
            VALUES  ('{$ip}','{$browser}','{$cnt}','{$request}','$input')
             ON DUPLICATE KEY UPDATE cnt=cnt+1,request=VALUES(request),input=VALUES(input),browser=VALUES(browser)
    EOT;
    
    //Запрос для истории
    $query2 = <<<EOT
        INSERT INTO wp_visiters_bot_history (`ip`,`browser`,`cnt`)
            VALUES  ('{$ip}','{$browser}','{$cnt}')
             ON DUPLICATE KEY UPDATE cnt=cnt+1,browser=VALUES(browser)
    EOT;
    
    
    $wpdb->query($query);
    
    $wpdb->query($query2);
    
    

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

  • Тепер страшне… Зараз мене спалять за мої дії 🙂
    Щоб записувати кожне звернення сайту, підключаємо код файлу в головний файл WordPress - wp-load.php. Так саме змінюємо файл ядра і саме після того, як вже існує глобальна змінна $wpdb

Отже, тепер ми можемо бачити як часто та чи інша IP адреса відзначається у нас у таблиці і з кухлем кави заглядаємо туди раз на 5 хвилин для розуміння картини.

Обчислюємо потенційних «злих» ботів і блокуємо їх по IP

Далі просто, скопіювали "шкідливий" IP, відкрили файл .htaccess і додали до кінця файлу

Order allow,deny
Allow from all
# start_auto_deny_list
Deny from 94.242.55.248
# end_auto_deny_list

Все, тепер 94.242.55.248 - не має доступу до сайту і не генерує навантаження на БД

Але щоразу так руками копіювати не дуже праведне заняття, та й до того ж код замислювався як автономний.

Додамо файл, який буде виконуватися CRON кожні 30 хвилин:

Код файлу, що модифікує .htaccess

<?php

/**
 * Файл автоматического задания блокировок по IP адресу
 * Должен запрашиваться через CRON
 */
if (empty($_REQUEST['key'])) {
    die('Hello');
}

require('wp-load.php');

global $wpdb;

$limit_cnt = 70; //Лимит запросов по которым отбирать

$deny_table = $wpdb->get_results("SELECT * FROM wp_visiters_bot WHERE cnt>{$limit_cnt}");

$new_blocked = [];

$exclude_ip = [
    '87.236.16.70'//адрес хостинга
];

foreach ($deny_table as $result) {

    if (in_array($result->ip, $exclude_ip)) {
        continue;
    }

    $wpdb->insert('wp_visiters_bot_blocked', ['ip' => $result->ip], ['%s']);
}

$deny_table_blocked = $wpdb->get_results("SELECT * FROM wp_visiters_bot_blocked");

foreach ($deny_table_blocked as $blocked) {
    $new_blocked[] = $blocked->ip;
}

//Очистка таблицы
$wpdb->query("DELETE FROM wp_visiters_bot");

//echo '<pre>';print_r($new_blocked);echo '</pre>';

$file = '.htaccess';

$start_searche_tag = 'start_auto_deny_list';

$end_searche_tag = 'end_auto_deny_list';

$handle = @fopen($file, "r");
if ($handle) {

    $replace_string = '';//Тест для вставки в файл .htaccess

    $target_content = false; //Флаг нужного нам участка кода

    while (($buffer = fgets($handle, 4096)) !== false) {

        if (stripos($buffer, 'start_auto_deny_list') !== false) {
            $target_content = true;
            continue;
        }

        if (stripos($buffer, 'end_auto_deny_list') !== false) {
            $target_content = false;

            continue;
        }

        if ($target_content) {
            $replace_string .= $buffer;
        }
    }
    if (!feof($handle)) {
        echo "Ошибка: fgets() неожиданно потерпел неудачуn";
    }
    fclose($handle);
}

//Текущий файл .htaccess
$content = file_get_contents($file);

$content = str_replace($replace_string, '', $content);

//Очищаем все блокировки в файле .htaccess
file_put_contents($file, $content);

//Запись новых блокировок
$str = "# {$start_searche_tag}" . PHP_EOL;

foreach ($new_blocked as $key => $value) {
    $str .= "Deny from {$value}" . PHP_EOL;
}

file_put_contents($file, str_replace("# {$start_searche_tag}", $str, file_get_contents($file)));

Код файлу досить простий і примітивний і головна його ідея в тому, щоб взяти кандидатів на блокування і вписати правила блокування в файл .htaccess між коментарями
# start_auto_deny_list та # end_auto_deny_list

Тепер "шкідливі" ip блокуються самі собою, а файл .htaccess виглядає приблизно так:

# BEGIN WordPress
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

# END WordPress

Order allow,deny
Allow from all

# start_auto_deny_list
Deny from 94.242.55.248
Deny from 207.46.13.122
Deny from 66.249.64.164
Deny from 54.209.162.70
Deny from 40.77.167.86
Deny from 54.146.43.69
Deny from 207.46.13.168
....... ниже другие адреса
# end_auto_deny_list

У результаті після початку дії такого коду можна побачити результат у хостинг панелі:

Обчислюємо потенційних «злих» ботів і блокуємо їх по IP

PS: Матеріал авторський, хоч його частину я й публікував на своєму сайті, але на Habre вийшла більш розширена версія.

Джерело: habr.com

Додати коментар або відгук