Как защитить формы регистрации от subscription bombing
Узнайте, как предотвратить использование вашего сервера для спам-атак. Мы разберем внедрение rate limits и подтверждение email через Double Opt-in для защиты пользователей.
Что такое subscription bombing?
Subscription bombing — это атака, при которой ваша форма регистрации используется для переполнения чужого почтового ящика. Злоумышленник берет адрес электронной почты жертвы и за короткий промежуток времени отправляет его в сотни или тысячи незащищенных форм. Каждый из этих сайтов отправляет на указанный адрес приветственное сообщение или письмо с подтверждением. В совокупности эти сообщения скрывают письма, которые жертве действительно необходимо прочитать.
Целью является владелец почтового ящика. Пока ящик заполняется подтверждениями подписок, злоумышленник тратит деньги с карты этого человека или сбрасывает пароль от одной из его учетных записей. Уведомление о мошенничестве от банка все равно приходит. Однако оно оказывается под двумя тысячами других сообщений, поступивших в течение того же часа, поэтому никто не замечает его вовремя.
Ваш сервер выступает инструментом, с помощью которого осуществляется атака. На вашем сервере ничего не сломано. Ни одна из ваших учетных записей не была скомпрометирована. Кто-то ввел адрес в публичную форму, и ваше программное обеспечение выполнило то, для чего оно было написано: отправило письмо на этот адрес. Именно поэтому такую атаку сложно обнаружить. В ваших логах нет следов вторжения, потому что самого вторжения не было.
Как эта атака выглядит с вашей стороны
Она проявляется в одном из двух видов.
Громкий вид — это всплеск. Несколько сотен POST-запросов к одной форме за несколько минут с множества различных IP-адресов. Они содержат адреса доменов, на которые вы никогда раньше не отправляли письма. Это легко заметить, если проверить логи.
Тихий вид — тот, который чаще всего пропускают. У злоумышленника есть список из тысяч уязвимых форм, поэтому ваша форма получает лишь одну или две заявки в час. Jye Cusch описал атаку именно такого типа на сайте, которым он управляет: никакого всплеска трафика, просто постоянные регистрации в часы, нехарактерные для его аудитории. Одна форма выглядит безобидно, так как она почти не проявляет активности. Ущерб складывается из суммы по всем формам из списка злоумышленника.
Оба вида атак имеют один общий признак впоследствии: никаких дальнейших действий. Адреса никогда не подтверждаются. Они не открывают письма и не переходят по ссылкам. В списке с подтвержденной подпиской (confirmed opt-in) они вечно остаются со статусом unconfirmed, и эта накопленная масса — самое явное доказательство, которое вы получите.
Начните с подсчета количества заявок в минуту в вашем access log.
sudo awk '/POST \/subscription\/form/ {print substr($4, 2, 17)}' \
/var/log/nginx/access.log | uniq -c | sort -rn | head$4 в стандартном формате combined log — это метка времени в квадратных скобках, поэтому данная команда выводит количество для каждой минуты, начиная с наибольшего. Если форма, которая обычно получает четыре регистрации в день, показывает шестьдесят за одну минуту, значит, с ней что-то не так.
Подтвержденная подписка: наиболее эффективная защита
Подтвержденная подписка, которую обычно называют double opt-in, означает, что адрес не считается подписанным, пока пользователь не перейдет по ссылке в письме, отправленном на этот адрес. Включите эту функцию, и каждый отправленный адрес приведет ровно к одному сообщению. Адрес не будет добавлен в список, поэтому он не получит рассылку или приветственную серию писем.
В listmonk, self-hosted сервере рассылок это настраивается для каждого списка отдельно: список может быть либо single opt-in, либо double opt-in. Документация прямо указывает на разницу. В списке с double opt-in подписчики «явно подтверждают подписку, переходя по ссылке в полученном письме. До этого момента они не получают сообщений рассылки». Подписчик находится в состоянии unconfirmed, переходит в confirmed после клика, и только подписчики confirmed в списке opt-in получают письма кампаний.
Будьте честны в отношении того, что это дает. Подтвержденная подписка не сводит ваш вклад в спам к нулю. Она ограничивает его одним сообщением на адрес. Жертва все равно получит это сообщение, а одно сообщение с каждой из тысячи площадок — это и есть вся атака. Подтвержденная подписка устраняет все, что следует за этим: ваш список остается чистым, и вы никогда не отправите второе сообщение тому, кто не просил о первом.
Важны еще две настройки, о которых легко забыть. Во-первых, ограничьте повторную отправку подтверждений. Если один и тот же адрес можно отправлять снова и снова, получая каждый раз новое письмо с подтверждением, атакующему не нужны тысячи форм, так как ваша форма сама отправит тысячу сообщений. Адрес, который уже находится в состоянии unconfirmed в этом списке, не должен получать ничего дополнительного как минимум в течение суток. Во-вторых, удаляйте неподтвержденные записи по расписанию. Адрес, который не подтвердил подписку в течение 30 дней, не является ожидающим подписчиком. Его хранение лишь создает риск того, что по ошибке на него будет отправлено письмо в будущем.
Ограничение частоты запросов к форме регистрации на reverse proxy
Размещайте ограничение перед приложением, а не внутри него. Запрос, заблокированный на прокси, не открывает соединение с базой данных и не инициирует сессию SMTP (simple mail transfer protocol). Ограничение внутри приложения срабатывает уже после того, как запрос занял рабочий процесс и выполнил запрос к БД; во многих стеках сообщение ставится в очередь до того, как будет выполнена проверка на злоупотребление. Ограничение на прокси продолжает работать даже при обновлении приложения, так как оно не зависит от кода, который вы заменяете.
Ниже приведен пример для nginx. Эта концепция применима к любому reverse proxy, который вы используете перед приложением, хотя названия директив будут отличаться.
Разместите это в блоке http, например, в файле /etc/nginx/conf.d/signup-limit.conf:
map $request_method $signup_key {
POST $binary_remote_addr;
default "";
}
limit_req_zone $signup_key zone=signup:10m rate=2r/m;
limit_req_status 429;
limit_req_log_level warn;map выполняет основную работу. nginx не учитывает запросы, ключ которых является пустой строкой, поэтому в зону попадают только POST-запросы. Пользователь, загружающий страницу регистрации несколько раз, не расходует лимит. Без этой карты человек, обновивший страницу дважды, исчерпал бы свой бюджет до того, как отправил бы данные.
$binary_remote_addr — это адрес клиента в упакованном виде, поэтому зона объемом 10 мегабайт вмещает примерно 160 000 записей. rate=2r/m разрешает одну отправку каждые тридцать секунд. limit_req_status 429 возвращает HTTP 429 Too Many Requests вместо стандартного для nginx 503; это корректный код ответа, который ожидают клиентские библиотеки.
Затем в блоке server для вашего сайта:
location = /subscription/form {
limit_req zone=signup burst=3 nodelay;
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}burst=3 nodelay позволяет пользователю, который дважды кликнул по кнопке, отправить запрос, но отклоняет четвертый запрос сразу, не ставя его в очередь.
sudo nginx -t && sudo systemctl reload nginxnginx -t должен вывести configuration file /etc/nginx/nginx.conf test is successful. Теперь быстро отправьте форму пять раз и следите за логом ошибок:
sudo tail -f /var/log/nginx/error.logЗаблокированный запрос записывает одну строку, и это именно та строка, которую вы ищете:
2026/08/13 09:14:22 [warn] 812#812: *4412 limiting requests, excess: 3.400 by zone "signup", client: 203.0.113.10, server: news.example.com, request: "POST /subscription/form HTTP/1.1", host: "news.example.com"Отсутствие строки означает, что ограничение не применяется. Частая причина заключается в том, что limit_req находится в блоке location, до которого запрос не доходит, поэтому проверьте конфигурацию с помощью curl -si -X POST https://news.example.com/subscription/form несколько раз подряд и убедитесь, что получаете 429.
Перед тем как полагаться на ограничение по IP, стоит учесть две ловушки.
При работе за CDN или другим прокси, $binary_remote_addr будет адресом этого прокси. Все посетители окажутся в одной корзине, поэтому первые несколько отправок в минуту заблокируют доступ для всех остальных. Исправьте это с помощью модуля real IP: set_real_ip_from для каждого из опубликованных диапазонов вашего CDN (Cloudflare публикует свои списки на cloudflare.com/ips) и real_ip_header CF-Connecting-IP. Подтвердите исправление, прочитав $remote_addr в вашем access log и убедившись, что там указан адрес посетителя, а не вашего CDN.
IPv6 делает ограничение по адресу слабым. $binary_remote_addr учитывает полный адрес /128, а домашняя сеть IPv6 обычно получает подсеть /64 или больше. Это гораздо больше адресов, чем может перебрать злоумышленник, и у каждого из них свой чистый бюджет. Добавьте вторую зону в качестве верхнего предела для самого эндпоинта, используя константу в качестве ключа, чтобы форма имела общий лимит независимо от того, сколько исходных адресов используется:
map $request_method $signup_total_key {
POST "signup";
default "";
}
limit_req_zone $signup_total_key zone=signup_total:1m rate=30r/m;Добавьте limit_req zone=signup_total burst=10 nodelay; в тот же location. Установите лимит выше вашего самого загруженного реального часа с запасом. Это грубый метод контроля: во время атаки он будет отклонять и реальные регистрации. Это оправданный компромисс, так как альтернативой является отправка почты вашим сервером.
Почему ограничение по адресу нельзя реализовать на уровне прокси
Адрес электронной почты передаётся в теле POST-запроса, а Nginx не выполняет парсинг тел запросов. Все переменные, по которым может работать limit_req_zone, извлекаются из строки запроса, заголовков или параметров соединения. Поэтому правило вида «этот адрес может получать не более одного подтверждения в день» должно находиться в первом компоненте, который считывает тело запроса, то есть в вашем приложении.
Не пытайтесь обойти это ограничение, перенося адрес в строку запроса, чтобы сделать его доступным для $arg_email. Это приведёт к записи адреса каждого подписчика в ваш access log в открытом виде, а также во все системы сбора логов, расположенные далее по цепочке. Вы замените проблему ограничения частоты запросов проблемой нарушения конфиденциальности.
Существует одно реальное исключение. Модуль Nginx JavaScript (njs) позволяет считывать тело запроса и устанавливать на его основе переменную, что даёт возможность создать ключ для ограничения по адресу на уровне прокси. Это вполне рабочий вариант, однако он добавляет новый код в путь обработки вашего запроса. Для большинства сайтов ограничение по адресу логичнее реализовать рядом с базой данных, которая уже «знает», есть ли у данного адреса ожидающее подтверждение, в то время как прокси должен заниматься ограничением по IP и по конечным точкам, с чем он справляется эффективно.
Не повторяйте отправленный текст в сообщении
Исключите из отправляемого сообщения любые строки, предоставленные пользователем. Существует две отдельные причины, обе из которых активно используются злоумышленниками.
Если в письме с подтверждением вы обращаетесь к читателю по имени, взятому из формы, злоумышленник может ввести свой текст в поле имени. Ваш сервер доставит этот текст жертве от имени вашего домена, подписав его вашим ключом DKIM (DomainKeys Identified Mail). Ваш сайт превратится в инструмент для рассылки чужих вредоносных сообщений, и почтовые провайдеры увидят ваш домен в качестве источника.
Вторая причина еще серьезнее. Если любое отправленное поле вручную добавляется в заголовок письма, символ переноса строки в этом поле позволяет злоумышленнику добавить произвольные заголовки, включая Bcc. Современные почтовые библиотеки отклоняют переносы строк в значениях заголовков. Однако код, передающий текст в sendmail через shell-скрипты, часто этого не делает.
Безопасное сообщение с подтверждением должно содержать название вашего сайта, одну ссылку и одно пояснительное предложение. Сам адрес должен присутствовать только там, где это необходимо агенту передачи почты — в заголовке To. Проверьте это: отправьте форму, указав в поле имени перенос строки и явную ссылку, затем прочитайте исходный код полученного сообщения с помощью less и убедитесь, что ни то, ни другое не попало в письмо.
Попутно настройте страницу успеха так, чтобы она отображала одинаковую информацию для любого адреса. Страница, которая сообщает «вы уже подписаны» для одного адреса и «проверьте входящие» для другого, превращает вашу форму в инструмент проверки членства для любого, у кого есть список адресов для тестирования.
Какую проверку на ботов выбрать?
Выбирайте метод с учетом доступности так же тщательно, как и с учетом эффективности. Капчу с выбором изображений не сможет пройти незрячий пользователь, а аудио-версия сложна для людей с обычным слухом. Проверка, из-за которой реальный пользователь не может зарегистрироваться, — это защита, которая сама становится препятствием. Ниже приведены четыре варианта в порядке их предпочтительности.
Proof of work в браузере. Браузер вычисляет хеш, который сервер может легко проверить; пользователю при этом ничего решать не нужно. listmonk предлагает этот метод в разделе Settings, затем Security, используя ALTCHA, для которой не требуется сторонний сервис. По состоянию на август 2026 года это основная рекомендация listmonk, пришедшая на смену устаревшему варианту hCaptcha. Затраты ресурсов ложатся на того, кто отправляет больше всего запросов, то есть на атакующего.
Управляемая неинтерактивная проверка. Cloudflare Turnstile в большинстве случаев ничего не показывает посетителю и выдает запрос только при подозрительных сигналах. Это эффективный метод, однако он добавляет сторонний сервис в процесс регистрации.
Поле-ловушка (honeypot). Текстовое поле, которое человек не видит, а примитивный бот заполняет. Дайте ему имя, которое не используется в форме, и установите autocomplete="off", tabindex="-1" и aria-hidden="true", чтобы менеджер паролей не заполнял его, а программа чтения с экрана не озвучивала. Поле с именем email2 или address будет автоматически заполнено браузером, из-за чего вы начнете отклонять реальных пользователей.
<div style="position:absolute; left:-9999px;" aria-hidden="true">
<label for="hp_ref">Leave this field empty</label>
<input type="text" id="hp_ref" name="hp_ref" autocomplete="off" tabindex="-1">
</div>Проверка времени отправки. При отрисовке страницы поместите подписанную метку времени в скрытое поле и отклоняйте отправку, если она поступила менее чем через две секунды. Человек не может прочитать форму и ввести адрес так быстро. Подписывайте метку времени, иначе бот просто отправит старую.
Что нужно проверить при выборе любого метода: токен должен использоваться только один раз. Если скрипт может решить задачу один раз и использовать этот токен для тысячи адресов, проверка лишь подтвердила, что один раз был запущен браузер, и не более того.
Как узнать об этом до получения жалобы на злоупотребление?
Вы должны узнавать о проблемах из собственных графиков, а не от отдела по работе со злоупотреблениями хостинг-провайдера. Отслеживайте два показателя.
Подсчитайте количество попыток отправки с каждого исходного адреса в логах:
sudo awk '/POST \/subscription\/form/ {print $1}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20Затем настройте fail2ban на чтение тех же строк limiting requests, которые уже записывает nginx, чтобы блокировать нарушителей. В состав fail2ban уже входит фильтр для этой задачи. Создайте /etc/fail2ban/jail.d/nginx-limit-req.local:
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
port = http,https
logpath = /var/log/nginx/error.log
findtime = 600
maxretry = 10
bantime = 3600sudo systemctl reload fail2ban
sudo fail2ban-client status nginx-limit-reqВывод статуса показывает фильтр тюрьмы (jail), а также количество неудачных попыток и заблокированных адресов. Значение Currently banned: 0 в спокойный день — это норма. Если тюрьма не отображается, значит, fail2ban не загрузил файл; команда sudo fail2ban-client -d | grep nginx-limit-req выведет конфигурацию, которую он фактически считал. Стандартный фильтр соответствует любой зоне limit_req. Ограничьте его вашей зоной регистрации, установив ngx_limit_req_zones = signup в секции [Definition] файла /etc/fail2ban/filter.d/nginx-limit-req.local. Структура файлов тюрем и команды блокировки подробно описаны в руководстве по fail2ban для Ubuntu 24.04.
Второй индикатор — это соотношение, для которого не требуется дополнительное ПО: количество заявок, делённое на количество подтверждений. В здоровом списке большинство пользователей, отправивших адрес, переходят по ссылке — обычно это более половины. Если это соотношение падает на фоне роста числа заявок, значит, ваш сервис используют в злонамеренных целях. Сравнивайте количество подписчиков unconfirmed, созданных за последний час, с количеством подписчиков confirmed согласно графику, по которому вы уже формируете отчеты.
Цена вопроса: репутация отправителя и черные списки
Этот аспект превращает досадную неприятность в реальные финансовые потери.
Списки адресов, используемые для рассылок, собираются автоматизированно, и такие списки содержат спам-ловушки. Это адреса, которые нигде не регистрировались и были опубликованы исключительно для выявления отправителей, рассылающих письма без согласия получателей. Ваше письмо с подтверждением попадает на такой адрес. Некоторым операторам черных списков достаточно одного такого факта.
Получатели, которые не запрашивали вашу рассылку, не будут нажимать кнопку отписки. Они нажмут «пожаловаться на спам». Правила Google для массовых отправителей, действующие с февраля 2024 года, требуют, чтобы отправители, рассылающие 5000 и более писем в день на адреса Gmail, поддерживали уровень жалоб на спам в Postmaster Tools ниже 0.3%. К более мелким отправителям это число формально не применяется, но те же сигналы о жалобах влияют на алгоритмы фильтрации, которые отправляют вашу почту в папку «Спам». Поддельные адреса в базе также вызывают жесткие отказы (hard bounce), а рост доли таких отказов является самостоятельным сигналом о плохой репутации для любого крупного почтового провайдера.
Если вы используете собственный почтовый сервер на VPS с mailcow, попадание в черный список затрагивает ваш IP-адрес и домен. Исключение из списков таких операторов, как Spamhaus, требует заполнения формы и ожидания. Пока вы ждете, ваши счета и письма сброса пароля также не будут доставлены. Если же вы отправляете почту через стороннего провайдера, будьте готовы к тому, что они сначала заблокируют ваш аккаунт, а уже потом будут читать ваши объяснения, так как ваш трафик создает риски для всех остальных отправителей на этом IP.
На фоне этих рисков объем необходимых работ невелик. Включите подтверждение подписки (confirmed opt-in) сегодня, так как это всего одна настройка для каждого списка. Затем добавьте ограничение частоты запросов (rate limit) на прокси, так как это требует правки одного файла и перезагрузки сервиса. Проверку ботов и настройку оповещений можно выполнить до конца недели.
FAQ
Помогает ли двойное подтверждение (double opt-in) против «бомбардировки» подписками?
Это предотвращает засорение вашего списка и ограничивает вклад злоумышленника одним сообщением на каждый отправленный адрес, что является самым эффективным доступным средством защиты. Это не останавливает переполнение почтового ящика жертвы, так как атака складывается из множества сообщений от тысяч разных сайтов. Используйте это в сочетании с ограничением частоты запросов (rate limit) по IP-адресу на вашем прокси и ограничением на повторную отправку писем с подтверждением, чтобы повторная отправка одного и того же адреса не приводила к генерации второго сообщения.
Как отличить «бомбардировку» от успешного дня с реальными подписками?
Смотрите на то, что происходит после отправки формы. Реальные пользователи подтверждают подписку, обычно в течение нескольких часов. «Бомбардировка» оставляет массив адресов, которые никогда не подтверждаются, не открываются и не переходят по ссылкам. Запросы при «бомбардировке» также имеют странные характеристики: множество неизвестных IP-адресов отправителей, домены получателей, на которые вы обычно не отправляете письма, и время поступления, равномерно распределенное по всему дню, а не соответствующее часам активности вашей аудитории.
Нужно ли удалять адреса, которые были отправлены?
Да. Удаляйте неподтвержденные записи старше примерно тридцати дней, и делайте это по расписанию, а не вручную. Никогда не отправляйте на эти адреса ничего другого, включая извинения или сообщения с вопросом «это были вы?», так как это будет вторым нежелательным письмом человеку, который уже завален ими. Если какой-либо из этих адресов является спам-ловушкой (spamtrap), последующее сообщение станет подтверждением, которого ждет оператор черного списка.
Будет ли ограничение частоты запросов (rate limiting) отклонять реальных подписчиков?
Ограничение в одну отправку каждые тридцать секунд с возможностью кратковременного всплеска до трех запросов незаметно для человека, заполняющего форму один раз. Оно становится заметным, когда множество реальных людей используют один адрес, например, в офисе за одним шлюзом NAT (network address translation), или когда ваш прокси видит адрес вашего CDN вместо адреса посетителя. Изучите $remote_addr в вашем access log перед тем, как ужесточать настройки, и держите лимит для конечной точки выше показателей вашего самого загруженного часа.
Мой IP-адрес отправителя попал в черный список после атаки. Что делать в первую очередь?
Прекратите отправку с этого IP-адреса, прежде чем предпринимать какие-либо действия. Приостановите очередь рассылки, исправьте форму и удалите неподтвержденные адреса, так как исключение из списка, за которым следует тот же самый трафик, приведет к повторному внесению в черный список быстрее, чем в первый раз. Затем выясните, в каком именно списке вы находитесь, так как у большинства операторов есть страница проверки по IP-адресу, и следуйте их процедуре удаления. Ожидание может занять несколько дней; используйте это время, чтобы убедиться, что ваша запись SPF (sender policy framework) и подпись DKIM по-прежнему проходят проверку.