Numa concessionária que atendi, o formulário de leads da home rodava em Elementor Pro, ao lado de um endpoint REST próprio aberto sem trava nenhuma. De fato, num dia só, um único IP: cerca de 203 envios pelo formulário e mais 447 leads falsos direto no endpoint. Além disso, tudo isso ia para o CRM da loja, virava post no WordPress e disparava e-mail para o time de vendas. Foi daí que veio a defesa que resolveu, com código no tema, sem plugin pago — é essa camada que mostro aqui.
Passo 1 — pegar o IP real atrás do proxy
Se o site fica atrás de um proxy ou de uma CDN, REMOTE_ADDR não é o IP de quem enviou o formulário — é o IP do proxy. Por isso, rate-limitar por esse valor bloqueia a CDN inteira, não o bot. Portanto, esse primeiro helper resolve isso lendo o header que a CDN preenche com o IP verdadeiro:
Passo 2 — rate-limit por IP com transient
Com o IP certo em mãos, um transient guarda quantos envios aquele IP fez nos últimos minutos. Ou seja, sem tabela nova, sem plugin — só get_transient() e set_transient(), que o WordPress já tem prontos. Por exemplo, o limite aqui é 5 envios a cada 10 minutos:
Passo 3 — honeypot e um detector de spam
Mas rate-limit sozinho não segura bot que troca de IP a cada envio. Nesse sentido, a segunda camada olha o conteúdo do formulário antes de aceitar o lead:
Passo 4 — plugar no formulário do Elementor
No Elementor Pro, o hook elementor_pro/forms/new_record roda antes do envio seguir para as integrações — CRM, e-mail, o que estiver configurado. Logo, é aí que entram o rate-limit e o detector de spam, cortando o envio cedo, sem chegar a lugar nenhum:
Passo 5 — travar o endpoint REST
Endpoint que cria dado nunca pode ter permission_callback igual a __return_true — isso é convite pro bot que já achou a URL. Por isso, troquei por um callback que aplica o mesmo rate-limit e confere se a chamada realmente sai do próprio site, comparando o host de origin ou referer com o do site:
Passo 6 — a rota que lista os leads é dado pessoal
Se existir uma rota GET que lista os leads, ela devolve nome, telefone e e-mail — dado pessoal, direto sob a LGPD. Nesse caso, o permission_callback dessa rota precisa exigir current_user_can('manage_options'), e, do mesmo modo, a consulta precisa paginar de verdade, nunca posts_per_page igual a -1 devolvendo tudo de uma vez:
Nunca deixe assim: permission_callback igual a __return_true numa rota que grava dado. Foi essa porta aberta que deixou o flood entrar sem barreira nenhuma.
Armadilhas
O que custou tempo de verdade nesse caso: