pular para o conteúdo

Camada anti-spam em formulários Elementor sem plugin pago

Capa: Camada anti-spam em formulários Elementor sem plugin pago

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:

				
					<?php
function meusite_client_ip() {
    // atrás do Cloudflare o IP verdadeiro vem neste header
    if ( ! empty( $_SERVER['HTTP_CF_CONNECTING_IP'] ) ) {
        return $_SERVER['HTTP_CF_CONNECTING_IP'];
    }
    return $_SERVER['REMOTE_ADDR'] ?? '';
}
				
			

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:

				
					function meusite_lead_rate_limited( $ip ) {
    $key   = 'lead_rate_' . md5( $ip );
    $count = (int) get_transient( $key );
    if ( $count >= 5 ) {
        return true;
    }
    set_transient( $key, $count + 1, 10 * MINUTE_IN_SECONDS );
    return false;
}
				
			

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:

  • Honeypot: um campo de texto escondido com CSS display:none e um id fixo. Humano nunca vê, nunca preenche; se vier preenchido, é bot.
  • E-mail: validado com is_email() antes de aceitar o lead.
  • Domínio descartável: lista de domínios de e-mail temporário reprova de cara.
  • Telefone: só passa com 10 a 13 dígitos depois de tirar tudo que não é número.
  • Nome: padrões-lixo (tudo maiúsculo, sequência de números, string aleatória) reprovam.

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:

				
					add_action( 'elementor_pro/forms/new_record', function( $record, $handler ) {
    $ip = meusite_client_ip();

    if ( meusite_lead_rate_limited( $ip ) ) {
        error_log( "lead bloqueado (rate limit): $ip" );
        $handler->add_error_message( 'Muitas tentativas. Tente de novo em alguns minutos.' );
        return;
    }

    $campos = array();
    foreach ( $record->get( 'fields' ) as $id => $campo ) {
        $campos[ $id ] = $campo['value'];
    }

    if ( meusite_lead_e_spam( $campos ) ) {
        error_log( "lead bloqueado (spam): $ip" );
        $handler->add_error_message( 'Não foi possível enviar o formulário.' );
        return;
    }
}, 10, 2 );
				
			

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:

				
					function meusite_leads_permission( $request ) {
    $ip = meusite_client_ip();
    if ( meusite_lead_rate_limited( $ip ) ) {
        return new WP_Error( 'rate_limited', 'Muitas requisições.', array( 'status' => 429 ) );
    }

    $home    = wp_parse_url( home_url(), PHP_URL_HOST );
    $origin  = $request->get_header( 'origin' );
    $referer = $request->get_header( 'referer' );

    $ok = ( $origin  && wp_parse_url( $origin,  PHP_URL_HOST ) === $home )
       || ( $referer && wp_parse_url( $referer, PHP_URL_HOST ) === $home );

    if ( ! $ok ) {
        return new WP_Error( 'invalid_origin', 'Origem inválida.', array( 'status' => 403 ) );
    }

    return true;
}
				
			

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:

				
					'permission_callback' => function () {
    return current_user_can( 'manage_options' );
},
'args' => array(
    'per_page' => array( 'default' => 20 ),
    'page'     => array( 'default' => 1 ),
),
				
			

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:

  • Rate-limit com REMOTE_ADDR cru atrás de CDN bloqueia o IP da CDN, não o do bot — sempre leia o header certo primeiro.
  • Logue todo bloqueio com error_log(). Sem isso não dá pra auditar depois quem foi barrado e por quê.
  • Isso é a camada de código. Um WAF ou a própria CDN na frente continuam valendo — defesa em profundidade, uma camada não substitui a outra.

Do mesmo assunto

Capa: Página 78x mais leve, Elementor vs PHP puro

Elementor, Tutoriais

6 ago 2026

Página 78x mais leve: quando trocar o Elementor por PHP puro

Cloneei a home de um site em Elementor puro e
Widgets de blocos atômicos do Elementor V4

Atualizações, Elementor, Tutoriais, Web Design, WordPress

7 fev 2026

Novos widgets de blocos aômicos do elementor V4

🚀 NOVIDADE NO ELEMENTOR V4! Os novos Widgets de Blocos
Tela de computador mostrando WordPress, Elementor e código PHP para banner global em multisite.

Desenvolvimento WordPress

22 abr 2025

Como usar esse shortcode dentro do Elementor com widgets de shortcode

🧩 Shortcodes personalizados no WordPress Multisite Os shortcodes são uma

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *