Ловушка для краулеров
Сайт сам порождает бесконечное множество адресов, поисковый робот обходит их сутками, а сервер честно отвечает «страница найдена». Разбираем, как это распознать и закрыть двумя правилами nginx.
Что происходит
На странице есть ссылка или скрипт, который дописывает к текущему адресу служебный параметр — не проверяя, что этот параметр там уже есть. Робот переходит по ссылке, попадает на ту же страницу с адресом чуть длиннее, находит на ней такую же ссылку — и так по кругу.
Каждый круг добавляет ещё один слой URL-кодирования: знак вопроса ? превращается в %3F, тот — в %253F, дальше в %25253F. Адрес растёт, страница остаётся той же самой.
Ключевой признак — внутри строки запроса появляется второй знак вопроса в любом виде кодирования. В корректном адресе такого быть не может: первый ? отделяет путь от параметров, дальше разделителем служит только &.
Почему это дорого
- Сервер. Каждый такой адрес — полноценная сборка страницы. На разобранном сайте это 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, bingbot | 200 | белый список UA, лимит не действует |
| Мониторинг, кроны сайта | 200 | белый список IP — важнее UA |
| python-requests, curl, Wget, Go-http | 403 | голая библиотека без белого IP |
| zgrab, masscan, sqlmap, Nikto | 403 | сканер уязвимостей |
| Censys, Shodan, InternetMeasurement | 403 | сканирование сети «для науки» |
| MJ12bot, AhrefsBot, PetalBot | 403 | SEO-краулер без отдачи |
| Запрос без User-Agent | 403 | браузеры так не ходят |
| Зациклённый адрес | 301 | ведём на чистый URL |
Четыре грабли
Обратный слэш в кавычках не работает. nginx снимает его на этапе разбора конфига: "\?" превращается в ? и становится квантификатором — правило проходит проверку nginx -t и молча не срабатывает. Писать символьный класс [?].
Кроны часто ходят через curl. А curl/ стоит в списке нежелательных. Без белого списка по IP вы заблокируете задания собственного сайта и не сразу это заметите — внешне всё работает. Сначала занести адреса, потом включать блокировку.
Один лимит на страницы и статику — это отказы у живых людей. Замер на странице каталога: 121 запрос в первую секунду — картинки, стили, скрипты. Под общей зоной посетитель упирается в лимит и получает 429 вместо половины картинок. Статике нужна отдельная свободная зона.
С доверенного адреса блокировку не проверить. С сервера и из офиса всегда придёт 200 — они в белом списке. Для проверки временно закомментировать нужную строку в блоке geo, прогнать тест, вернуть обратно.
Порядок внедрения на сайт
- Снять долю зациклённых адресов по логу — команда из шага «Как проверить любой сайт». Записать цифру до изменений.
- Определить виновный параметр по своему логу, а не по образцу из инструкции.
- Собрать белый список IP: сам сервер, мониторинг, офис, платёжные callback'и, обмен с 1С.
- Сделать резервные копии конфигов с датой в имени.
- Добавить правила обрыва ловушки. Проверить: одиночный параметр — 200, повтор — 301.
- Добавить карты и блокировку ботов. Проверить снаружи, с адреса вне белого списка.
- Проверить, что проходят: живой браузер, Googlebot, YandexBot, мониторинг, кроны.
- Открыть тяжёлую страницу каталога в браузере и убедиться, что 429 нет ни на одном ресурсе.
- Дописать Disallow в robots.txt.
- Через час снять статистику: сколько 403 и 301, и главное — сколько 429. Ноль последних означает, что живых не задело.
- Предупредить заказчика: посещаемость в метриках упадёт. Это уходит мусор, а не покупатели.
Составлено по результатам работ на боевом сайте 23.08.2026. Конфигурация проверена на nginx 1.30, Ubuntu 24.04, FastPanel. Списки User-Agent и адресов дополняются по логам конкретного сайта.