Spam z formularza kontaktowego to najbardziej widoczny, ale najmniej groźny efekt działania botów. Przy projektach, którymi się opiekuję, częściej widzę masowe zakładanie kont, testowanie wykradzionych haseł na formularzu logowania, wysyłanie tysięcy SMS-ów przez formularz weryfikacji czy zapytania do API, które w kilka godzin zużywają miesięczny limit płatnej usługi. W tym wpisie pokazuję, jak buduję ochronę warstwami, tak żeby zatrzymać automaty, a prawdziwy użytkownik w ogóle jej nie zauważył.
Warstwy ochrony i kiedy je stosuję
| Warstwa | Co zatrzymuje | Wpływ na użytkownika |
|---|---|---|
| Honeypot i kontrola czasu | proste skrypty wypełniające wszystkie pola | żaden |
| Limity zapytań | masowe wysyłanie z jednego adresu lub konta | żaden, dopóki ktoś nie przekroczy limitu |
| Weryfikacja w tle (Turnstile, reCAPTCHA v3) | boty udające przeglądarkę | zwykle żaden, czasem dodatkowe sprawdzenie |
| Reguły w CDN lub WAF | ruch z sieci botów, znanych źródeł ataków i nietypowych krajów | żaden przy dobrze ustawionych regułach |
| Weryfikacja przy podejrzanym ruchu | ataki przechodzące przez pozostałe warstwy | dodatkowy krok tylko dla podejrzanych sesji |
Na zwykłej stronie firmowej wystarczają mi pierwsze dwie warstwy. Przy sklepach, formularzach rejestracji i publicznych API dokładam kolejne.
Honeypot i kontrola czasu wypełnienia
Honeypot to dodatkowe pole niewidoczne dla ludzi. Proste boty wypełniają wszystkie pola, które znajdą w kodzie, więc wypełnione pole-pułapka jednoznacznie wskazuje automat:
<div class="hp" aria-hidden="true">
<label for="company_site">Strona firmy</label>
<input type="text" id="company_site" name="company_site" tabindex="-1" autocomplete="off">
</div>
Pole ukrywam w CSS przez przesunięcie poza ekran, a nie display: none, które część botów rozpoznaje. Atrybuty
aria-hidden i tabindex="-1" sprawiają, że nie trafi na nie czytnik ekranu ani użytkownik przechodzący klawiszem
Tab, a autocomplete="off" chroni przed automatycznym wypełnieniem przez przeglądarkę.
Drugą prostą kontrolą jest czas. Przy generowaniu formularza zapisuję w podpisanym polu znacznik czasu, a przy odbiorze odrzucam zgłoszenia wysłane szybciej niż po 2–3 sekundach. Człowiek nie wypełni formularza w ułamku sekundy, a bot zwykle tak. Na serwerze odrzucam takie zgłoszenia bez komunikatu o błędzie, żeby nie podpowiadać, co zadziałało.
Limity zapytań do formularzy i API
Każdy endpoint, który wysyła wiadomość, zakłada konto, loguje lub kosztuje pieniądze, ma u mnie limit zapytań. Limit liczę nie tylko po adresie IP, bo boty często zmieniają adresy, ale też po koncie, adresie e-mail lub numerze telefonu, którego dotyczy akcja. Przykład dla Express.js:
import rateLimit from 'express-rate-limit';
const contactLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minut
limit: 5, // 5 zgłoszeń z jednego adresu
standardHeaders: 'draft-7',
legacyHeaders: false,
});
app.post('/api/contact', contactLimiter, handleContact);
Przy kilku serwerach aplikacji liczniki trzymam we wspólnej pamięci, np. w Redisie, bo inaczej każdy serwer liczyłby osobno. Po przekroczeniu
limitu zwracam kod 429 z nagłówkiem Retry-After. Te same mechanizmy, rozbudowane o opóźnienia i blokady kont, chronią formularz
logowania, a ich dobór to podstawa ochrony przed atakami brute force.
Weryfikacja w tle zamiast zagadek obrazkowych
Klasyczna CAPTCHA z przepisywaniem liter czy klikaniem w obrazki obniża konwersję formularza i bywa nie do przejścia dla osób z niepełnosprawnościami. Dlatego korzystam z rozwiązań działających w tle, które oceniają sesję na podstawie zachowania i sygnałów przeglądarki:
- Cloudflare Turnstile wybieram najczęściej. Jest bezpłatny, nie wymaga przenoszenia strony do Cloudflare i nie pokazuje zagadek obrazkowych.
- Google reCAPTCHA v3 zwraca ocenę od 0,0 do 1,0, a o progu odrzucenia decyduję sam. Stosuję go, gdy klient korzysta już z innych usług Google, pamiętając o informacji w polityce prywatności i ograniczeniach darmowego planu.
W obu przypadkach najważniejsza jest weryfikacja tokenu na serwerze. Sam widżet na stronie niczego nie chroni, jeśli backend nie sprawdzi odpowiedzi w API dostawcy, a bot może wysłać żądanie bezpośrednio do endpointu, z pominięciem formularza.
Ochrona na poziomie serwera i CDN
- Reguły w CDN lub WAF. Przy stronie za Cloudflare ustawiam osobne reguły dla ścieżek formularzy i API, np. dodatkowe sprawdzenie dla ruchu z sieci centrów danych. Pilnuję, żeby nie blokowały one robotów wyszukiwarek na stronach, które mają być indeksowane.
- Sprawdzanie pochodzenia żądań. Formularze zmieniające dane chronię tokenem i weryfikacją nagłówka
Origin, tak samo jak przy ochronie przed atakami CSRF. - Nagłówki odpowiedzi. Polityka CORS ograniczona do własnych domen i nagłówki bezpieczeństwa HTTP utrudniają wykorzystanie API z obcych stron.
Jak rozpoznaję, że boty atakują formularz
Ataki botów rzadko są widoczne w panelu CMS. Zauważam je zwykle w danych: nagły wzrost liczby rejestracji z jednej domeny e-mail, setki nieudanych logowań w ciągu godziny, wysyłki SMS z prefiksów zagranicznych czy rosnące koszty usług zewnętrznych. Dlatego przy formularzach, które coś kosztują, ustawiam alerty na liczbę zdarzeń w czasie, a w razie podejrzeń sięgam po analizę logów serwera, która pokazuje adresy, częstotliwość i wzorce zapytań. Na tej podstawie dobieram reguły tak, żeby blokowały konkretny atak, a nie przypadkowych użytkowników.