SSD Nodes Learn 🎉 VPS від $5.50/міс
Посібники Matt ConnorВід Matt Connor

Як зупинити bombing підписок у формах реєстрації

Зловмисник надсилає адресу жертви до сотень форм одночасно. Confirmed opt in і rate limits не дають серверу відправити всі листи підтвердження.

Що таке bombing підписками?

Bombing підписками — це атака, під час якої вашу форму реєстрації використовують, щоб переповнити чужу поштову скриньку. Зловмисник бере адресу електронної пошти жертви та протягом короткого часу надсилає її до сотень або тисяч незахищених форм. Кожен із цих сайтів надсилає на цю адресу вітальне повідомлення або повідомлення для підтвердження. Разом ці повідомлення приховують листи, які жертві справді потрібно прочитати.

Мішень атаки — власник цієї поштової скриньки. Поки скринька заповнюється підтвердженнями підписок, зловмисник витрачає кошти з картки цієї людини або скидає пароль до одного з її облікових записів. Сповіщення банку про шахрайську операцію все одно надходить. Але воно опиняється під двома тисячами інших повідомлень, які надійшли протягом тієї самої години, тому його ніхто не встигає побачити.

Ваш сервер є інструментом, за допомогою якого здійснюється атака. На вашій системі нічого не зламано. Жоден ваш обліковий запис не було скомпрометовано. Хтось ввів адресу в загальнодоступну форму, а ваше програмне забезпечення виконало призначену йому функцію: надіслало лист на цю адресу. Саме тому таку атаку важко помітити. У ваших журналах немає слідів вторгнення, оскільки вторгнення не було.

Як виглядає атака з вашого боку

Вона має одну з двох форм.

Помітна форма — це сплеск. За кілька хвилин на одну форму надходять кілька сотень POST-запитів із багатьох різних IP-адрес джерела. Запити містять адреси доменів, на які ви раніше ніколи не надсилали повідомлень. Цю атаку легко побачити, якщо перевірити журнал.

Непомітну форму часто пропускають. Зловмисник має список із тисяч вразливих форм, тому вашій формі достатньо обробляти один або два запити на годину. Jye Cusch описав атаку саме такого типу на сайті, яким він керує: сплеску трафіку не було, лише регулярно надходили реєстрації в години, що не відповідали поведінці його аудиторії. Окрема форма виглядає безпечною, оскільки майже не створює навантаження. Шкода є сумарним результатом атак на всі форми зі списку зловмисника.

Після обох типів атак залишається одна спільна ознака: далі нічого не відбувається. Адреси ніколи не підтверджуються. Користувачі не відкривають повідомлення й не натискають посилання. У списку з підтвердженням підписки вони назавжди залишаються зі статусом unconfirmed, і це найочевидніший доказ, який ви можете отримати.

Почніть із підрахунку кількості надсилань за хвилину в журналі доступу.

sudo awk '/POST \/subscription\/form/ {print substr($4, 2, 17)}' \
  /var/log/nginx/access.log | uniq -c | sort -rn | head

$4 у стандартному combined-форматі журналу — це час у квадратних дужках. Тому команда виводить кількість запитів для кожної хвилини, починаючи з найбільшого значення. Якщо форма, яка зазвичай отримує чотири реєстрації на день, за одну хвилину отримала шістдесят, це не нормальна активність.

Підтверджена підписка: захід із найбільшим ефектом

Підтверджена підписка, яку зазвичай називають подвійною підпискою, означає, що адреса не стає підписником, доки людина не натисне посилання в повідомленні, надісланому на цю адресу. Увімкніть цей режим, і одна передана адреса породить рівно одне повідомлення за весь час. Адреса не потрапить до списку, тому не отримуватиме ні кампаній, ні вітальної серії повідомлень.

У listmonk, self-hosted сервері розсилок це налаштування рівня списку: список використовує одинарну або подвійну підписку. Документація чітко пояснює різницю. У списку з подвійною підпискою підписники «явно підтверджують підписку, натискаючи посилання в листі з підтвердженням, який вони отримують. До цього вони не отримують повідомлень кампаній». Підписник перебуває у стані unconfirmed, після натискання переходить у стан confirmed, і лише підписники у стані confirmed в списку з підтвердженням отримують листи кампаній.

Потрібно чесно оцінювати переваги цього заходу. Підтверджена підписка не зменшує ваш внесок до нуля. Вона обмежує його одним повідомленням на адресу. Жертва все одно отримує це повідомлення, а одне повідомлення від кожного з тисячі сайтів — це вже повноцінна атака. Підтверджена підписка усуває все, що відбувається після цього: ваш список залишається чистим, і ви ніколи не надсилаєте друге повідомлення тому, хто не просив навіть першого.

Важливі ще два налаштування, про які легко забути. По-перше, обмежте повторні надсилання повідомлення з підтвердженням. Якщо ту саму адресу можна повторно передавати у формі й щоразу отримувати ще один лист із підтвердженням, зловмиснику не потрібна тисяча форм, оскільки сама ваша форма надішле тисячу повідомлень. Адреса, яка вже перебуває у стані unconfirmed у цьому списку, не повинна отримувати нічого додаткового щонайменше протягом доби. По-друге, видаляйте непідтверджені записи за розкладом. Адреса, яка не підтвердила підписку протягом тридцяти днів, не є підписником, що очікує підтвердження. Її зберігання лише створює ризик, що пізніше їй випадково буде надіслано повідомлення.

Обмеження частоти запитів до форми реєстрації на reverse proxy

Встановлюйте обмеження перед застосунком, а не всередині нього. Запит, заблокований на проксі, не відкриває підключення до бази даних і не починає SMTP (simple mail transfer protocol) сеанс. Обмеження всередині застосунку спрацьовує після того, як запит уже витратив процес worker і виконав запит до бази даних. У багатьох стеках повідомлення також потрапляє в чергу до перевірки на зловживання. Обмеження на проксі зберігається після оновлення застосунку, оскільки воно не міститься в коді, який ви замінюєте.

Нижче наведено приклад для 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-запити. Кілька відкривань сторінки реєстрації не витрачають ліміт. Без цієї map користувач, який двічі оновив сторінку, витратив би власний ліміт ще до надсилання форми.

$binary_remote_addr — це адреса клієнта в упакованому форматі, тому зона розміром 10 megabyte вміщує приблизно 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 nginx

nginx -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 — це адреса цього проксі. Усі відвідувачі потрапляють в один bucket, тому перші кілька надсилань за хвилину блокують усіх інших. Виправте це за допомогою модуля визначення реальної IP-адреси: додайте set_real_ip_from для кожного опублікованого діапазону вашого CDN (Cloudflare публікує їх на сторінці cloudflare.com/ips) і додайте real_ip_header CF-Connecting-IP. Перевірте виправлення, переглянувши $remote_addr у журналі доступу. Там має бути адреса відвідувача, а не адреса CDN.

IPv6 робить обмеження для окремої адреси слабким. $binary_remote_addr зберігає повну адресу /128, а домашня IPv6-мережа зазвичай має префікс /64 або більший. Це набагато більше адрес, ніж зловмисник може використати. Кожна з них має власний невикористаний ліміт. Додайте другу зону як загальне обмеження для самого endpoint. Використайте сталу ключову ознаку, щоб форма мала загальну граничну частоту незалежно від кількості вихідних адрес:

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. У такому разі адреси всіх підписників записуватимуться у журнал доступу у відкритому вигляді, а також у будь-який сервіс пересилання журналів, підключений після нього. Ви заміните обмеження частоти запитів проблемою конфіденційності.

Є один реальний виняток. Модуль JavaScript для nginx, njs, може читати тіло запиту та встановлювати на його основі змінну. Це дає змогу створити ключ для окремої адреси на проксі. Це повноцінний варіант, але він також додає новий код до шляху обробки запиту. Для більшості сайтів обмеження для окремої адреси має бути поруч із базою даних, яка вже знає, чи є для цієї адреси незавершене підтвердження. Проксі натомість має обробляти обмеження для окремих IP-адрес і кінцевих точок, для яких він добре підходить.

Не повторюйте надісланий текст у повідомленні

Не включайте жоден рядок, наданий зловмисником, до повідомлення, яке ви надсилаєте. Для цього є дві окремі причини, і обидві вже використовувалися на практиці.

Якщо у вітальному листі ви звертаєтеся до одержувача за ім’ям, отриманим із форми, зловмисник вводить свій текст у поле імені. Потім сервер доставляє цей текст жертві від вашого домену та підписує його вашим ключем DKIM (DomainKeys Identified Mail). Ваш сайт стає сервісом доставки чужих зловмисних повідомлень, а поштовий провайдер одержувача бачить у них ваш домен.

Друга причина серйозніша. Якщо якесь поле форми вручну додається до поштового заголовка, символ нового рядка в цьому полі додає заголовки на вибір зловмисника, зокрема Bcc. Сучасні поштові бібліотеки відхиляють символи нового рядка у значеннях заголовків. Код, який зі shell-скрипту передає текст до sendmail, часто цього не робить.

Безпечне повідомлення-підтвердження містить назву вашого сайту, одне посилання та одне речення з поясненням. Сама адреса з’являється лише там, де вона потрібна агенту передавання пошти, — у заголовку To. Перевірте це: надішліть форму, вказавши в полі імені символ нового рядка та очевидне посилання, потім прочитайте отримане необроблене повідомлення за допомогою less і переконайтеся, що жоден із цих елементів не зберігся.

Також зробіть так, щоб сторінка успішного надсилання показувала однаковий текст для будь-якої адреси. Сторінка, яка для однієї адреси повідомляє «ви вже підписані», а для іншої — «перевірте вхідні повідомлення», перетворює вашу форму на засіб перевірки членства для будь-кого, хто має список адрес для перевірки.

Яку перевірку для ботів слід використовувати?

Обирайте засіб захисту з таким самим урахуванням доступності, як і ефективності. CAPTCHA з вибором зображень недоступна для незрячих користувачів, а аудіозапасний варіант складно пройти людям зі звичайним слухом. Якщо через перевірку легітимний користувач не може зареєструватися, це захист, який також створює витрати. Ось чотири варіанти в рекомендованому порядку.

Proof of work у браузері. Браузер обчислює хеш, який сервер може дешево перевірити, тому користувачеві не потрібно нічого розв’язувати. listmonk надає цю можливість у розділі Settings, потім Security, використовуючи ALTCHA, для якого не потрібен сторонній сервіс. Станом на August 2026 це власна рекомендація listmonk замість застарілого варіанта hCaptcha. Витрати несе той, хто надсилає найбільше запитів, тобто зловмисник.

Керована неінтерактивна перевірка. Cloudflare Turnstile зазвичай нічого не показує більшості відвідувачів і запускає перевірку лише тоді, коли його сигнали вказують на підозрілу активність. Це ефективний варіант, але він додає сторонній сервіс до процесу реєстрації.

Поле-пастка. Це текстове поле, якого користувач не бачить, але яке заповнює простий бот. Дайте йому ім’я, яке більше ніде у вашій формі не використовується, і задайте autocomplete="off", tabindex="-1" та aria-hidden="true", щоб password manager не заповнював це поле, а screen reader його не озвучував. Поле з іменем 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  = 3600
sudo systemctl reload fail2ban
sudo fail2ban-client status nginx-limit-req

У виведенні стану зазначено фільтр jail, а також поточну кількість невдалих спроб і заблокованих адрес. Значення Currently banned: 0 у спокійний день є нормальним. Якщо jail взагалі не відображається, 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. Структуру файлів jail і команди блокування докладніше описано в посібнику з fail2ban для Ubuntu 24.04.

Другий сигнал — це співвідношення, для якого не потрібне нове програмне забезпечення: кількість надсилань, поділена на кількість підтверджень. У здоровому списку більшість людей, які надсилають адресу, натискають посилання — зазвичай значно більше половини. Якщо це співвідношення різко падає, а кількість надсилань зростає, ваш сервіс використовують для зловживань. Порівнюйте кількість створених за останню годину підписників unconfirmed із кількістю confirmed підписників, використовуючи розклад, за яким ви вже формуєте звіти.

Ціна для вас: репутація відправника та blocklist

Це те, що перетворює проблему на рахунок до сплати.

Списки адрес, які використовують для bombing, збирають автоматично. У таких списках є spamtrap-адреси: вони ніколи ніде не реєструвалися й публікуються лише для виявлення відправників, які надсилають повідомлення без дозволу. Ваше повідомлення для підтвердження потрапляє на одну з таких адрес. Деяким операторам blocklist для цього більше нічого не потрібно.

Отримувачі, які не запитували ваше повідомлення, не натискають «відписатися». Вони натискають «поскаржитися на спам». Правила Google для масових відправників, чинні з February 2024, вимагають відправникам 5,000 або більше повідомлень на день до Gmail підтримувати частку скарг на спам у Postmaster Tools нижче 0.3%. Менший відправник не оцінюється за цим показником, але той самий сигнал скарг впливає на фільтрацію, через яку ваші листи потрапляють у папку зі спамом. Несправжні адреси в межах атаки також спричиняють hard bounce, а зростання частки hard bounce є окремим сигналом репутації в кожного великого провайдера.

Якщо ви використовуєте власний поштовий сервер на VPS із mailcow, запис потрапляє до вашої IP-адреси та домену. Щоб видалити запис у такого оператора, як Spamhaus, потрібно заповнити форму й чекати. Поки ви чекаєте, ваші рахунки та листи для скидання паролів також не доставляються. Якщо натомість ви надсилаєте пошту через спільного провайдера, очікуйте, що спочатку він призупинить ваш обліковий запис, а вже потім прочитає ваше пояснення. Ваш трафік створює ризик для всіх інших відправників із цієї IP-адреси.

На цьому тлі обсяг роботи невеликий. Увімкніть confirmed opt-in сьогодні, оскільки для цього потрібне одне налаштування на список. Потім додайте обмеження швидкості на проксі, оскільки для цього потрібен один файл і reload. Перевірку bot і сповіщення можна додати протягом цього тижня.

FAQ

Чи зупиняє подвійне підтвердження підписки subscription bombing?

Воно не дає забруднити ваш список і обмежує ваш внесок одним повідомленням на кожну подану адресу. Це найбільше окреме покращення, доступне у вашій ситуації. Воно не зупиняє переповнення вхідної пошти жертви, оскільки атака складається з одного повідомлення від кожного з тисячі сайтів. Поєднайте цей захист з обмеженням швидкості для кожної IP-адреси на проксі та обмеженням кількості повторних надсилань підтвердження. Тоді повторне подання тієї самої адреси не створюватиме другого повідомлення.

Як відрізнити атаку від справжнього сплеску реєстрацій?

Перевірте, що відбувається після надсилання форми. Реальні підписники підтверджують підписку, зазвичай протягом кількох годин. Під час атаки накопичується список адрес, які не підтверджують підписку, не відкривають повідомлення і не переходять за посиланнями. Подання також мають нетиповий розподіл: багато вихідних адрес, яких ви раніше не бачили, домени одержувачів, яким ви зазвичай не надсилаєте повідомлення, і час надходження, рівномірно розподілений протягом доби, а не прив’язаний до годин активності вашої аудиторії.

Чи слід видаляти подані адреси?

Так. Видаляйте непідтверджені записи, яким близько тридцяти днів або більше, і робіть це за розкладом, а не вручну. Ніколи не надсилайте цим адресам нічого іншого, зокрема вибачення або повідомлення «це були ви?», оскільки це буде другим непроханим повідомленням для людини, яку вже завалили такими повідомленнями. Якщо серед цих адрес є spamtrap, подальше повідомлення стане підтвердженням, на яке очікує оператор blocklist.

Чи відхилятиме обмеження швидкості справжніх підписників?

Обмеження для однієї IP-адреси на рівні одного подання кожні тридцять секунд із burst до трьох запитів непомітне для людини, яка один раз заповнює форму. Воно стає помітним, коли багато реальних людей використовують одну адресу, наприклад в офісі за одним шлюзом NAT (network address translation), або коли проксі бачить адресу CDN, а не адресу відвідувача. Перед посиленням обмежень прочитайте $remote_addr у журналі доступу та встановіть максимальну кількість запитів до рівня, вищого за ваш найактивніший реальний час.

Мою IP-адресу для надсилання внесли до blocklist після атаки. Що зробити спочатку?

Спочатку припиніть надсилання з цієї адреси. Призупиніть чергу кампанії, виправте форму та видаліть непідтверджені адреси, оскільки після виключення з blocklist повторний такий самий трафік призведе до повторного внесення швидше, ніж уперше. Потім з’ясуйте, до якого списку внесено адресу: більшість операторів мають сторінку перевірки, де пошук виконується за вашою IP-адресою. Дотримуйтеся їхньої процедури видалення. Очікуйте, що процес триватиме кілька днів. Використайте цей час, щоб перевірити, чи й далі успішно проходять перевірку ваш запис SPF (sender policy framework) і підписування DKIM.

#email#double-opt-in#rate-limiting#abuse#deliverability