Помощь

Ловушка для краулеров

Сайт сам порождает бесконечное множество адресов, поисковый робот обходит их сутками, а сервер честно отвечает «страница найдена». Разбираем, как это распознать и закрыть двумя правилами nginx.

83% суточного трафика приходилось на ловушку
94 558 запросов из 113 007 за сутки
851 оборвано за первый час после правки
0 живых посетителей задето

Что происходит

На странице есть ссылка или скрипт, который дописывает к текущему адресу служебный параметр — не проверяя, что этот параметр там уже есть. Робот переходит по ссылке, попадает на ту же страницу с адресом чуть длиннее, находит на ней такую же ссылку — и так по кругу.

Каждый круг добавляет ещё один слой URL-кодирования: знак вопроса ? превращается в %3F, тот — в %253F, дальше в %25253F. Адрес растёт, страница остаётся той же самой.

Норма
/kostanay/
Обычная страница региона
Круг 0
/kostanay/?HOME_PAGE=YES
Служебный параметр шаблона. Законен, нужен для AJAX
Круг 1
/kostanay/?HOME_PAGE=YES%3Fbxrand%3D1785888000000
Скрипт дописал параметр через «?» вместо «&» — начало роста
Круг 2
/kostanay/?HOME_PAGE=YES%3FHOME_PAGE%3DYES
Параметр продублировался внутри собственного значения
Круг 4
/kostanay/?HOME_PAGE=YES%3FHOME_PAGE%3DYES%253FHOME_PAGE%253DYES%25253FHOME_PAGE%25253DYES
Слои кодирования нарастают, конца у последовательности нет

Ключевой признак — внутри строки запроса появляется второй знак вопроса в любом виде кодирования. В корректном адресе такого быть не может: первый ? отделяет путь от параметров, дальше разделителем служит только &.

Почему это дорого

  • Сервер. Каждый такой адрес — полноценная сборка страницы. На разобранном сайте это 94 тысячи лишних сборок в сутки при 18 тысячах осмысленных.
  • Поисковая выдача. Робот тратит краулинговый бюджет на мусор вместо карточек товаров. Все эти адреса — дубли одной страницы, и сервер отвечает на них 200, то есть подтверждает: страница существует, индексируйте.
  • Аналитика. Цифры посещаемости раздуты в разы, решения принимаются по недостоверным данным.
  • Диагностика. Логи забиты мусором, реальные проблемы в них не видны.

Основным потребителем на разобранном сайте оказалась краулерная сеть Meta: около 1700 запросов с каждого из десятка адресов подсети. Формально это не атака — робот добросовестно идёт по ссылкам, которые ему дал сам сайт.

Как проверить любой сайт

Одна команда по логу доступа. Считает долю запросов, у которых внутри строки запроса встречается второй знак вопроса:

# подставить свой путь к логу
LOG=/var/log/nginx/access.log

TOTAL=$(grep -ac . "$LOG")
LOOP=$(grep -acE '"[A-Z]+ [^ "]*\?[^ "]*(\?|%3F|%253F)' "$LOG")
echo "зациклённых: $LOOP из $TOTAL"

Если доля заметная — посмотреть, какие именно адреса и какой параметр виноват:

grep -aoE '"[A-Z]+ [^ "]*\?[^ "]*(\?|%3F|%253F)[^ "]*' "$LOG" \
  | sed 's/"[A-Z]* //' | cut -c1-90 | sort | uniq -c | sort -rn | head -20

В выдаче будет виден повторяющийся параметр. На Битриксе это обычно HOME_PAGE и bxrand; на других движках — идентификаторы сессий, ссылки «вернуться назад», параметры фильтров и сортировок, календарные архивы. Имя параметра всегда берём из своего лога, а не из этого текста.

Что добавить в nginx

Правила ставятся в server-блок сайта, выше остальных location. Ответ — 301 на чистый адрес без параметров: робот получает однозначный сигнал «настоящая страница вот здесь», накопленные дубли постепенно склеиваются.

Адресный вариант — рекомендуемый

# Повтор параметра, в том числе внутри собственного значения
if ($args ~* "HOME_PAGE.*HOME_PAGE")           { return 301 $scheme://$host$uri; }

# «Зародыш» роста: знак вопроса сразу после значения параметра
if ($args ~* "HOME_PAGE=[^&]*([?]|%3F|%253F)") { return 301 $scheme://$host$uri; }

# Повтор второго участвующего параметра
if ($args ~* "bxrand.*bxrand")                 { return 301 $scheme://$host$uri; }

Одно вхождение параметра оставляем рабочим — оно нужно самому шаблону. Обрываем только повтор и рост.

Универсальный вариант — если параметров много

# Любой вложенный знак вопроса в строке запроса считаем поломкой
if ($args ~ "([?]|%3F|%253F)") { return 301 $scheme://$host$uri; }
Проверить перед включением универсального варианта

Есть законные адреса, где %3F внутри значения допустим: параметры с вложенной ссылкой — back_url, backurl, redirect, return_url, а также рекламные метки. Прогнать по логу командой из шага «Как проверить любой сайт» и убедиться, что среди совпадений нет ничего живого. Если есть — брать адресный вариант.

robots.txt — для послушных роботов

Disallow: /*HOME_PAGE=
Disallow: /*?bxrand=

Это дополнение, а не замена: часть роботов файл игнорирует, поэтому правило в nginx остаётся основным заслоном.

Заодно: фильтр сканеров

Ставится тем же заходом и той же базой. Две части: карты в http-контексте и их применение в конфиге сайта.

/etc/nginx/conf.d/bot-protection.conf

# Доверенные адреса. Заполнить своими — иначе отрежете собственный мониторинг.
geo $oh_trusted {
    default           0;
    127.0.0.1/32      1;
    IP_СЕРВЕРА/32     1;   # кроны сайта ходят curl'ом!
    IP_МОНИТОРИНГА/32 1;   # Zabbix, Blackbox-Exporter
    IP_ОФИСА/32       1;
}

# Полезные боты — пропускаем всегда
map $http_user_agent $oh_good_bot {
    default 0;
    "~*(Googlebot|Google-InspectionTool|Storebot-Google|AdsBot-Google)" 1;
    "~*(YandexBot|YandexMetrika|YandexRenderResourcesBot|YandexImages|YandexDirect)" 1;
    "~*(bingbot|BingPreview|Applebot|DuckDuckBot|Mail\.RU_Bot)" 1;
    "~*(facebookexternalhit|Twitterbot|TelegramBot|WhatsApp|Slackbot|vkShare)" 1;
}

# Нежелательные: сканеры уязвимостей, «научные» сканеры сети,
# SEO-краулеры без пользы, голые http-библиотеки, пустой UA
map $http_user_agent $oh_bad_bot {
    default 0;
    "" 1;
    "~*(zgrab|masscan|zmap|nmap|nikto|sqlmap|wpscan|nuclei|httpx|dirbuster|gobuster|acunetix|netsparker|qualys|openvas)" 1;
    "~*(Censys|Shodan|InternetMeasurement|Expanse|NetSystemsResearch|CriminalIP|leakix|Stretchoid|BinaryEdge|ProjectDiscovery)" 1;
    "~*(MJ12bot|DotBot|AhrefsBot|SemrushBot|BLEXBot|DataForSeo|Barkrowler|SeekportBot|serpstat|ZoominfoBot|PetalBot|Bytespider|MegaIndex)" 1;
    "~*(python-requests|python-httpx|aiohttp|Go-http-client|libwww-perl|Wget/|Scrapy|Java/|okhttp|PostmanRuntime|curl/|WinHttp)" 1;
}

# Ключ лимита: пусто = без ограничения (доверенные и поисковики)
map "$oh_trusted$oh_good_bot" $oh_limit_key {
    default $binary_remote_addr;
    "~^1"   "";
    "~^01"  "";
}

limit_req_zone $oh_limit_key zone=oh_req:16m    rate=10r/s;
limit_req_zone $oh_limit_key zone=oh_static:16m rate=100r/s;
limit_req_status 429;

В server-блоке сайта

# Порядок важен: сначала запрет, затем исключения снимают его
set $oh_deny 0;
if ($oh_bad_bot)  { set $oh_deny 1; }
if ($oh_good_bot) { set $oh_deny 0; }
if ($oh_trusted)  { set $oh_deny 0; }
if ($oh_deny)     { return 403; }

limit_req zone=oh_req burst=40 nodelay;

# В блоке статики — своя, свободная зона
location ~* \.(jpg|jpeg|png|gif|svg|webp|ico|css|js|woff2?|ttf|otf|pdf)$ {
    limit_req zone=oh_static burst=300 nodelay;
    expires 30d;
}
Кто Ответ Основание
Живой браузер200не подпадает ни под одну карту
Googlebot, YandexBot, bingbot200белый список UA, лимит не действует
Мониторинг, кроны сайта200белый список IP — важнее UA
python-requests, curl, Wget, Go-http403голая библиотека без белого IP
zgrab, masscan, sqlmap, Nikto403сканер уязвимостей
Censys, Shodan, InternetMeasurement403сканирование сети «для науки»
MJ12bot, AhrefsBot, PetalBot403SEO-краулер без отдачи
Запрос без User-Agent403браузеры так не ходят
Зациклённый адрес301ведём на чистый URL

Четыре грабли

Обратный слэш в кавычках не работает. nginx снимает его на этапе разбора конфига: "\?" превращается в ? и становится квантификатором — правило проходит проверку nginx -t и молча не срабатывает. Писать символьный класс [?].

Кроны часто ходят через curl. А curl/ стоит в списке нежелательных. Без белого списка по IP вы заблокируете задания собственного сайта и не сразу это заметите — внешне всё работает. Сначала занести адреса, потом включать блокировку.

Один лимит на страницы и статику — это отказы у живых людей. Замер на странице каталога: 121 запрос в первую секунду — картинки, стили, скрипты. Под общей зоной посетитель упирается в лимит и получает 429 вместо половины картинок. Статике нужна отдельная свободная зона.

С доверенного адреса блокировку не проверить. С сервера и из офиса всегда придёт 200 — они в белом списке. Для проверки временно закомментировать нужную строку в блоке geo, прогнать тест, вернуть обратно.

Порядок внедрения на сайт

  1. Снять долю зациклённых адресов по логу — команда из шага «Как проверить любой сайт». Записать цифру до изменений.
  2. Определить виновный параметр по своему логу, а не по образцу из инструкции.
  3. Собрать белый список IP: сам сервер, мониторинг, офис, платёжные callback'и, обмен с 1С.
  4. Сделать резервные копии конфигов с датой в имени.
  5. Добавить правила обрыва ловушки. Проверить: одиночный параметр — 200, повтор — 301.
  6. Добавить карты и блокировку ботов. Проверить снаружи, с адреса вне белого списка.
  7. Проверить, что проходят: живой браузер, Googlebot, YandexBot, мониторинг, кроны.
  8. Открыть тяжёлую страницу каталога в браузере и убедиться, что 429 нет ни на одном ресурсе.
  9. Дописать Disallow в robots.txt.
  10. Через час снять статистику: сколько 403 и 301, и главное — сколько 429. Ноль последних означает, что живых не задело.
  11. Предупредить заказчика: посещаемость в метриках упадёт. Это уходит мусор, а не покупатели.

Составлено по результатам работ на боевом сайте 23.08.2026. Конфигурация проверена на nginx 1.30, Ubuntu 24.04, FastPanel. Списки User-Agent и адресов дополняются по логам конкретного сайта.

Остались вопросы - пишите