Skuteczne zabezpieczenie formularza WordPress nie wymaga captchy, która irytuje klientów i obniża konwersję. W tym wpisie pokazuję pięć warstw ochrony, które wdrożyliśmy na własnej stronie — z gotowym kodem PHP, bez ani jednej wtyczki. Boty odbijają się cicho, a człowiek nie widzi żadnej różnicy.
Dlaczego nie captcha
Captcha przerzuca koszt spamu na Twojego klienta: każe mu klikać w przejścia dla pieszych, zanim będzie mógł zapytać o wycenę. Badania konwersji od lat pokazują to samo — każde dodatkowe pole i każda przeszkoda w formularzu to utracone zapytania. Tymczasem 95%+ spamu w formularzach WordPress generują proste boty, które nie wykonują JavaScriptu i wysyłają POST natychmiast po pobraniu strony. Do ich odsiania captcha jest niepotrzebna.
Zasada: warstwy zamiast jednej bramki
Zamiast jednego „strażnika” stosujemy pięć niezależnych filtrów. Każdy jest trywialny do przejścia? Być może. Ale wszystkie pięć naraz — już nie. I kluczowy detal całości: spam nigdy nie dostaje komunikatu błędu. Bot, który wpadł w pułapkę, widzi „wiadomość wysłana” — więc nie wie, co go zdradziło, i nie uczy się obchodzić zabezpieczeń.
// Udawany sukces: spam dostaje "wysłano", żeby bot nie uczył się, co go zdradziło.
$pretend = static function () use ( $redirect ) {
wp_safe_redirect( add_query_arg( 'sent', '1', $redirect ) );
exit;
};
Warstwa 1 — honeypot
Ukryte stylami pole „Website”, którego człowiek nie widzi i nie wypełni. Bot wypełniający wszystkie pola formularza — wypełni i to. Jeśli pole ma wartość, wiadomość ląduje w koszu.
if ( ! empty( $_POST['website'] ) ) {
$pretend(); // bot wypełnił ukryte pole
}
Warstwa 2 — pułapka czasowa
Człowiek potrzebuje czasu na wypełnienie formularza. Bot wysyła POST w ułamku sekundy. Przy renderze formularza zapisujemy w ukrytym polu znacznik czasu podpisany HMAC-em — podpis gwarantuje, że bot nie podstawi własnej wartości.
// Render: podpisany znacznik czasu.
$ts = time();
$sig = hash_hmac( 'sha256', (string) $ts, wp_salt( 'nonce' ) );
// <input type="hidden" name="ts" value="<?php echo esc_attr( $ts . '|' . $sig ); ?>">
// Obsługa: wysyłka szybsza niż 4 sekundy = bot.
list( $ts, $sig ) = array_pad( explode( '|', $raw_ts, 2 ), 2, '' );
$sig_ok = hash_equals( hash_hmac( 'sha256', $ts, wp_salt( 'nonce' ) ), $sig );
$age = time() - (int) $ts;
if ( ! $sig_ok || $age < 4 || $age > DAY_IN_SECONDS ) {
$pretend();
}
Dlaczego 4 sekundy? Testowaliśmy — nawet ktoś wklejający gotową treść potrzebuje więcej. Górna granica (doba) odcina boty odtwarzające raz przechwycony formularz tygodniami.
Warstwa 3 — token JavaScript
Proste boty nie wykonują JavaScriptu. Wykorzystujemy to: formularz zawiera puste ukryte pole, które dopiero skrypt strony uzupełnia wartością z atrybutu data-k. Brak wartości po stronie serwera = brak przeglądarki = bot.
// JS strony — jedna linijka:
document.querySelectorAll('input[name="js_token"]').forEach(function (el) {
el.value = el.getAttribute('data-k') || '';
});
Wartość tokenu również wyliczamy HMAC-em z tego samego znacznika czasu, więc serwer weryfikuje ją bez sesji i bez bazy danych.
Warstwa 4 — limit wysyłek na IP
Nawet bot z pełną przeglądarką nie powinien móc zalać skrzynki. Trzy wysyłki na kwadrans z jednego adresu IP wystarczą każdemu klientowi; czwarta i kolejne odbijają się cicho. Implementacja na transientach WordPressa — zero dodatkowych tabel:
$key = 'rl_' . md5( $_SERVER['REMOTE_ADDR'] ?? '' );
$hits = (int) get_transient( $key );
if ( $hits >= 3 ) {
$pretend();
}
set_transient( $key, $hits + 1, 15 * MINUTE_IN_SECONDS );
Warstwa 5 — filtr treści
Spam, który przejdzie wszystko powyżej, prawie zawsze zdradza się treścią: linki. Więcej niż dwa adresy URL w wiadomości albo jakikolwiek link w polu imienia czy tematu — do kosza.
if ( preg_match_all( '#https?://|www\.#i', $message ) > 2
|| preg_match( '#https?://|www\.|\[url#i', $name . ' ' . $topic ) ) {
$pretend();
}
Pułapki wdrożeniowe
Trzy rzeczy, o które łatwo się potknąć. Po pierwsze: cache stron. Jeśli serwujesz formularz z cache’a, znacznik czasu „starzeje się” razem ze stroną — dlatego górny limit ustawiliśmy na dobę, a nie na godzinę. Po drugie: serwer za proxy lub Cloudflare widzi w REMOTE_ADDR adres proxy, nie klienta — limit IP zliczałby wtedy wszystkich razem; skonfiguruj przekazywanie prawdziwego adresu na poziomie serwera. Po trzecie: nie loguj odrzuconego spamu do bazy „na wszelki wypadek” — po miesiącu będziesz mieć tabelę większą niż cała reszta strony.
Efekt
Pięć warstw, około 60 linii PHP i jedna linia JavaScriptu. Zero wtyczek, zero zewnętrznych usług, zero utrudnień dla klienta. Całość pokryliśmy testami — każda warstwa osobno plus scenariusz poprawnej wysyłki. Jeśli mimo to kiedyś przebije się bot z pełną przeglądarką i ludzkim tempem pisania — wtedy, i dopiero wtedy, sięgniemy po niewidzialną weryfikację typu Turnstile.
Potrzebujesz podobnego wdrożenia albo masz formularz, który właśnie toną w spamie? Odezwij się — odpowiadamy w jeden dzień roboczy.