Guias

Guias18 min de leitura

Como acabar com o spam no formulário de contato

Acabe com o spam no formulário de contato com camadas que não custam nada a pessoas reais: honeypot, tempo, pontuação de conteúdo, limites de taxa e verificação humana só quando precisar.

Preet SutharFundador, Ikiform

Empilhe verificações baratas e invisíveis antes de adicionar qualquer atrito: um campo honeypot oculto, um tempo mínimo para preencher o formulário, uma pontuação para links e frases de spam e um limite de taxa por visitante. Adicione uma verificação humana como o Cloudflare Turnstile só quando o spam ainda passar, e valide o token dela num servidor. Guarde o que você barrar em vez de jogar fora, para poder resgatar uma ou outra mensagem real.

Um formulário de contato numa página pública começa a receber lixo poucos dias depois de ser indexado. A maior parte nem é dirigida a você. Scripts varrem a web atrás de qualquer <form>, preenchem todos os campos e enviam, na esperança de que algum link pegue. Uma parte menor vem de navegadores reais controlados por software, e outra de pessoas pagas para colar o mesmo discurso em milhares de formulários. Cada tipo é barrado por uma verificação diferente, e é por isso que um truque só nunca resolve.

Este guia explica cada defesa, o que ela barra e quanto custa para visitantes reais, com código que roda em qualquer stack. Depois mostra como o Ikiform, um backend de formulários, aplica as mesmas camadas para você.

Camadas, da mais barata para a mais cara

Ordene as verificações pelo custo. Contar requisições é quase de graça, então isso roda antes de qualquer leitura do corpo. Verificações que chamam outro serviço, como um captcha, rodam depois que as baratas passam. A pontuação do conteúdo vem por último, porque precisa dos campos já interpretados. O Ikiform roda as verificações nessa ordem em todo envio.

A ordem das verificações de spam, da chegada do envio até ser guardado ou marcado como spamRecusado na horaPontuado e guardado de qualquer formachega umenviode uma página,script ouservidorlimite detaxademais de umvisitante:429domíniospermitidosenviado deoutro site?verificaçãohumanatoken doTurnstile, seativadohoneypotpreenchido:+5tempo econteúdorápidodemais, links,frases,repetiçõespontuação5 oumais?recebidoe-mail ewebhooksdisparadosspammesmaresposta desucesso, nadaenviadocontagemorigemtokennãosimA ordem das verificações de spam, da chegada do envio até ser guardado ou marcado como spamRecusado na horaPontuado e guardado de qualquer formachega um enviode uma página,script ou servidorlimite de taxademais de umvisitante: 429domíniospermitidosenviado de outrosite?verificaçãohumanatoken do Turnstile,se ativadohoneypotpreenchido: +5tempo e conteúdorápido demais,links, frases,repetiçõespontuação 5ou mais?recebidoe-mail e webhooksdisparadosspammesmaresposta desucesso,nadaenviadocontagemorigemtokennãosim
  1. Um envio chega ao endpoint do formulário.
  2. O limite de taxa conta primeiro os envios por visitante. Uma enxurrada é recusada com 429 antes de o corpo ser lido.
  3. Os domínios permitidos comparam a página de onde veio o envio com os domínios que você listou.
  4. Se a verificação humana estiver ativa, o envio precisa trazer um token válido do Turnstile.
  5. O honeypot soma 5 à pontuação quando é preenchido.
  6. Tempo, links, frases de spam conhecidas, endereços descartáveis e repetições somam valores menores.
  7. Com 5 ou mais, o envio é guardado como spam. Abaixo disso, o envio é recebido e seu e-mail e seus webhooks são disparados.
A linha de cima recusa envios. A de baixo nunca recusa: ela pontua, então um falso positivo se resolve com um clique para restaurar, sem perder a mensagem.

As defesas comparadas

Toda defesa tem um preço. A tabela é honesta sobre o que cada uma deixa passar e o que ela exige das pessoas de quem você quer ouvir.

DefesaBarraDeixa passarCusto para visitantes reais
HoneypotScripts que preenchem todos os camposBots que leem CSS ou pulam campos ocultos, e humanosNenhum, se ficar oculto para leitores de tela e preenchimento automático
Armadilha de tempoEnvios mais rápidos do que alguém consegue digitarBots lentos e bots que esperamNenhum, desde que o mínimo seja de poucos segundos
Pontuação de conteúdo, como contagem de links, frases de spam ou AkismetOfertas de SEO, links soltos, spam de cassino e de remédiosSpam com cara de limpo, e pode marcar uma mensagem real com linksNenhum, se você revisar o que ela barra
Rate limitingEnxurradas e scripts em loopSpam lento e espalhadoEscritórios e eventos atrás de um único IP podem bater num limite apertado
Domínios permitidosEnvios mandados direto para o seu endpoint de outro lugarScripts que falsificam o cabeçalho OriginNenhum
Cloudflare TurnstileA maioria dos navegadores automatizadosSpammers humanos pagosGeralmente invisível. Às vezes um clique, e exige um script
reCAPTCHA ou hCaptchaA maioria dos navegadores automatizadosSpammers humanos pagos e serviços de resoluçãoQuebra-cabeças de imagem para alguns visitantes, mais um script de terceiros

Comece pelas cinco primeiras. Elas são invisíveis e barram a maior parte do que um site pequeno recebe. Adicione uma verificação humana quando o painel mostrar que o spam ainda passa, não antes: é a única camada com um custo que todo visitante pode sentir. A página sobre acabar com o spam sem captcha mostra até onde as camadas invisíveis chegam sozinhas.

Leia o spam antes de combatê-lo

Olhe dez mensagens de spam antes de escolher uma solução. O que elas têm em comum diz quem as envia, e isso aponta para a camada que as barra.

Da cara do spam até a camada que o barratexto aleatório emtodo campoum bot de scriptlê o HTML, preenche tudohoneypotdezenas por minuto,uma origemum script em looplimite de taxaenvios limpos 1 sapós carregarum navegadorheadlessarmadilha de tempo,verificação humanaofertas educadas comlinksuma pessoa, paga porformuláriopontuação deconteúdo, revisãoO que você vêQuem enviaO que barraDa cara do spam até a camada que o barratexto aleatório em todocampoum bot de scriptlê o HTML, preenche tudohoneypotdezenas por minuto, umaorigemum script em looplimite de taxaenvios limpos 1 s apóscarregarum navegador headlessarmadilha de tempo,verificação humanaofertas educadas comlinksuma pessoa, paga porformuláriopontuação de conteúdo,revisão
  1. Texto aleatório em todos os campos vem de um script que preenche tudo. Um honeypot pega.
  2. Dezenas de envios por minuto de uma mesma origem vêm de um script em loop. Um limite de taxa barra.
  3. Envios bem-feitos mandados um segundo após a página carregar vêm de um navegador headless. Uma armadilha de tempo ou uma verificação humana barra.
  4. Ofertas educadas com links vêm de pessoas pagas por formulário. Só a pontuação de conteúdo e a sua própria revisão pegam.
Nenhum captcha barra a última linha. Uma pessoa consegue resolvê-lo, então a pontuação de conteúdo e uma olhada semanal na pasta de spam são a única defesa.

Um formulário de contato com as camadas integradas

Aqui está um formulário de contato, montado para cada stack. Toda versão tem o honeypot fora da tela e uma verificação humana. As versões só de navegador usam a verificação Turnstile gerenciada do Ikiform, que dispensa chaves. As versões de servidor mostram o caminho faça-você-mesmo: seu servidor valida um token do Turnstile com suas próprias chaves da Cloudflare e depois repassa os campos. Escolha sua stack abaixo; a escolha vale para todos os exemplos de código destes guias.

As abas de servidor compartilham dois pequenos helpers. verifyTurnstile pergunta à Cloudflare se um token é real, e sendToIkiform repassa os campos com sua chave secreta e o cabeçalho Origin do visitante. Um fetch do lado do servidor não envia Origin próprio, e o Ikiform o lê para reconhecer um teste gratuito em localhost. Os dois falham de forma fechada: se a Cloudflare não responder, o visitante vê um erro em vez de passar em silêncio.

lib/spam.ts
const SITEVERIFY = "https://challenges.cloudflare.com/turnstile/v0/siteverify";
const ENDPOINT = "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx";

export interface Contact {
  name: string;
  email: string;
  message: string;
  _gotcha: string;
}

export function readContact(data: FormData): Contact {
  return {
    name: String(data.get("name") ?? "").trim(),
    email: String(data.get("email") ?? "").trim(),
    message: String(data.get("message") ?? "").trim(),
    _gotcha: String(data.get("_gotcha") ?? ""),
  };
}

export async function verifyTurnstile(input: { secret: string; token: string; ip?: string | null }) {
  if (!input.token || input.token.length > 2048) return false;
  const body = new URLSearchParams({ secret: input.secret, response: input.token });
  if (input.ip) body.set("remoteip", input.ip);
  try {
    const response = await fetch(SITEVERIFY, { method: "POST", body });
    const result = (await response.json()) as { success?: boolean };
    return result.success === true;
  } catch {
    return false;
  }
}

export async function sendToIkiform(secretKey: string, contact: Contact, origin?: string | null) {
  try {
    const response = await fetch(ENDPOINT, {
      method: "POST",
      headers: {
        Accept: "application/json",
        "Content-Type": "application/json",
        Authorization: `Bearer ${secretKey}`,
        ...(origin ? { Origin: origin } : {}),
      },
      body: JSON.stringify(contact),
    });
    const result = await response.json().catch(() => null);
    if (result?.ok) return { ok: true as const };
    const fields: Record<string, string[]> = result?.error?.fields ?? {};
    const first = Object.values(fields)[0]?.[0];
    return {
      ok: false as const,
      status: response.status,
      message: String(first ?? result?.error?.message ?? "Couldn't send your message. Try again."),
    };
  } catch {
    return { ok: false as const, status: 502, message: "Couldn't reach the form service. Try again." };
  }
}

O formulário simples não precisa de script. Defina o Spam check do formulário no Ikiform como Ikiform, e um envio sem token recebe uma breve página de verificação em ikiform.com que roda o Turnstile e repassa os campos. Quem passa de forma invisível nem percebe.

contact.html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
  <label for="name">Name</label>
  <input id="name" name="name" autocomplete="name" required maxlength="120" />

  <label for="email">Email</label>
  <input id="email" name="email" type="email" autocomplete="email" required />

  <label for="message">Message</label>
  <textarea id="message" name="message" rows="5" required minlength="10" maxlength="5000"></textarea>

  <div class="hp" aria-hidden="true">
    <label for="hp-field">Leave this empty</label>
    <input id="hp-field" name="_gotcha" type="text" tabindex="-1" autocomplete="off" />
  </div>

  <button>Send message</button>
</form>

<style>
.hp {
  position: absolute;
  left: -10000px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}
</style>

O honeypot fica fora da tela em vez de atrás de display: none, porque alguns bots pulam campos ocultos desse jeito. O aria-hidden do contêiner e o tabindex do campo mantêm leitores de tela e teclados longe dele.

  • Toda versão usa a classe .hp do bloco de estilo da aba HTML. Coloque-a no seu CSS global.
  • O honeypot se chama _gotcha, o padrão do Ikiform. As versões de servidor o repassam sem alteração, então um preenchido ainda cai na sua pasta de spam com o motivo honeypot.
  • O Turnstile adiciona ao formulário um campo oculto cf-turnstile-response. Um token vale por 300 segundos e funciona uma única vez, então cada tentativa precisa de um novo.
  • As chaves de teste da Cloudflare permitem rodar tudo isso localmente. O guia do Turnstile lista quais são.
As versões do lado do servidor enviam com uma chave secreta, então o Ikiform pula Allowed domains e a verificação humana dele. Todo envio passa a vir do IP do seu servidor, e o limite Standard de 5 por minuto por visitante conta todos os seus visitantes juntos. Escolha o limite de taxa Relaxed ou Custom para um formulário enviado por um servidor e mantenha IKIFORM_SECRET_KEY em variáveis de ambiente só do servidor.

Teste a definição de campos abaixo. Ela roda o validador do Ikiform no seu navegador, com as mesmas regras que o servidor aplica antes de calcular qualquer pontuação de spam.

Prévia

Roda o validador do Ikiform no seu navegador. Nada é enviado.

Os mesmos campos do modelo de formulário de contato. Cole a definição na aba Fields do formulário e o Ikiform recusa envios que não batem com ela com 422 validation_failed.

O honeypot

Um honeypot é um campo que as pessoas nunca veem e os bots preenchem. Se tiver um valor, o envio veio de um software. Não custa nada aos visitantes e pega os bots mais antigos e comuns, os que leem o HTML e preenchem todo campo que encontram.

Escondê-lo bem importa mais que o truque em si. Três coisas dão errado com honeypots descuidados:

  • Leitores de tela o anunciam. Um visitante cego ouve um campo a mais, preenche e é marcado como spam. Envolva-o em aria-hidden="true" e dê a ele um rótulo dizendo para deixá-lo vazio.
  • O teclado para nele. Navegar pelo formulário com Tab para num campo invisível. tabindex="-1" o tira da ordem de tabulação.
  • O preenchimento automático o preenche. Navegadores e gerenciadores de senha preenchem campos pelo nome. Chame o campo de email2 ou phone e alguns vão preenchê-lo para pessoas reais. Use um nome que nenhum preenchimento automático reconheça, como _gotcha, e adicione autocomplete="off".

O guia do honeypot cobre técnicas para escondê-lo, como os bots o detectam e a verificação no servidor para cada stack. No Ikiform, um honeypot preenchido soma 5 à pontuação, o que já atinge sozinho o limite de 5. Você pode renomear o campo por formulário em Honeypot field.

Tempo

Pessoas levam segundos para preencher um formulário de contato. Muitos bots levam milissegundos. Uma armadilha de tempo registra quando o formulário foi renderizado e confere o intervalo quando ele é enviado. A versão ingênua coloca um timestamp num campo oculto:

Armadilha de tempo, a versão ingênua
const started = Number(formData.get("started"));
const seconds = (Date.now() - started) / 1000;
const tooFast = !Number.isFinite(seconds) || seconds < 3;

Um bot pode enviar o número que quiser, então um timestamp simples só barra bots que nem se dão ao trabalho. Assine-o no servidor com um segredo, e o bot não consegue fingir uma renderização mais lenta. A página sobre acabar com o spam sem captcha monta a versão assinada.

O Ikiform assina os horários de renderização para você quando o formulário vem do SDK ou do componente shadcn/ui: eles enviam _ikiform_ts, e um envio feito menos de 2 segundos após a renderização soma 3 à pontuação. Um timestamp forjado ou de um dia atrás soma 3. Formulários HTML simples não enviam um e não são penalizados.

Pontuando o conteúdo

Parte do spam é digitada ou colada por pessoas, então passa por todas as verificações de bot. O que o entrega é o conteúdo: links, frases de venda, endereços descartáveis e o mesmo texto duas vezes. Pontue cada sinal e trate um total alto como spam, em vez de bloquear por um sinal isolado. Um link numa mensagem é normal; um link no campo de nome nunca é.

SinalO Ikiform soma
O honeypot foi preenchido+5
Um link num campo de nome ou assunto+3
Os mesmos campos enviados a este formulário nas últimas 24 horas+3
Enviado menos de 2 segundos após o horário de renderização assinado+3
Três ou mais links somando todos os campos+2
Uma de 22 frases de spam conhecidas, como "seo services"+2
Um endereço num domínio de e-mail descartável+2

Com 5 ou mais, o envio é spam. Nenhum sinal leve chega lá sozinho: uma mensagem real com três links soma 2. Dois juntos muitas vezes chegam, como um link no nome e uma frase de spam. Se você mesmo pontuar o conteúdo, o Akismet é a opção hospedada de longa data; muitas equipes começam com algumas expressões regulares como as de cima.

Uma verificação humana

Uma verificação humana pede ao navegador que prove que é controlado por uma pessoa. As modernas, como o Cloudflare Turnstile, quase sempre rodam de forma invisível e só mostram uma caixa de seleção quando estão em dúvida. O widget entrega um token ao navegador; seu servidor precisa enviar esse token ao provedor para validá-lo. Um token que nunca é validado num servidor não barra nada, porque um bot pode pular o widget e enviar sem ele.

O Ikiform tem três opções em Spam check em cada formulário:

OpçãoO que aconteceMudanças no código
Off (the default)Nenhum desafio. O honeypot, o tempo e a pontuação continuam rodando.Nenhum
IkiformTurnstile rodado pelo Ikiform em ikiform.com, sem conta nem chaves da CloudflareNenhuma para envios HTML. Envios com fetch chamam antes challenge() de @ikiform/sdk
Your own Turnstile keys (suas próprias chaves)Seu widget e sua chave secreta. O Ikiform valida o token e confere o hostname dele com seus domínios permitidosRenderize o widget no seu formulário para que ele envie cf-turnstile-response

Um envio com fetch sem token recebe 403 challenge_required. Se a Cloudflare não responder, o Ikiform mantém o envio e o guarda como spam com o motivo captcha_unavailable, para você poder restaurá-lo. A documentação de spam mostra os dois fluxos. Se você está comparando o Turnstile com o reCAPTCHA do Google ou o hCaptcha, veja alternativas ao reCAPTCHA.

Limites de taxa e domínios permitidos

Um limite de taxa restringe a frequência com que um visitante pode enviar. Ele não distingue spam de mensagem legítima; ele barra volume. Conte por IP e formulário, permita uma pequena rajada e responda 429 com um cabeçalho Retry-After. O guia de limite de taxa monta um para cada stack e explica por que um contador em memória falha em hosts serverless.

O Ikiform conta cada envio com exatidão, antes de ler o corpo. Cada formulário escolhe um preset: Standard, 5 por minuto por visitante, Relaxed, 20 por minuto por visitante, ou Strict, 2 a cada 10 minutos por visitante. O Standard também permite no máximo 3 a cada 10 segundos, e o formulário inteiro aceita 300 por minuto. Um formulário de contato combina com o Strict, já que ninguém honesto escreve duas vezes em dez minutos. Os números estão em Limites de taxa.

Allowed domains lista os sites onde seu formulário fica. Um envio de qualquer outra página é marcado como spam, ou recusado com 403 origin_not_allowed se você escolher Reject. Isso impede que outras pessoas apontem os formulários delas para o seu endpoint. Um script determinado pode falsificar o cabeçalho que é conferido, então trate isso como uma camada, não como uma tranca.

O que acontece com o spam

Nunca diga a um bot que ele foi pego. Um erro ensina o que mudar; uma página de sucesso normal não ensina nada. E nunca apague spam na chegada: todo filtro pega uma mensagem real de vez em quando, e uma apagada se perde.

Um envio julgado como spam recebe a mesma resposta de sucesso que um real, então o bot não aprende nada. Ele é guardado com o status spam, não dispara e-mail de notificação nem webhook, não conta no seu uso e é apagado após 30 dias. Toda segunda-feira, um workspace que bloqueou spam recebe um único e-mail de resumo com o total, os formulários mais visados e os principais motivos.

Abra o filtro de status do painel e escolha Spam para ver cada um com sua pontuação e seus motivos. Not spam o devolve, e o e-mail e os webhooks dele são disparados nesse momento. Desative o e-mail de segunda-feira em Weekly spam summary nas configurações do workspace. O spam fica guardado por 30 dias, então confira pelo menos uma vez por mês.

Se uma mensagem real continua sendo pega, leia os motivos dela. Um link num campo chamado business_name conta como link num nome; renomeie o campo para business e ele deixa de contar como campo de nome.

Escolha o seu caso

Cada uma destas páginas se aprofunda numa defesa ou num tipo de site, com código para esse caso.

Confira se funciona

Rode estes testes num formulário de teste antes de confiar nele. O Ikiform não tem plano gratuito. Testar é grátis: um workspace sem plano aceita 50 envios de teste por mês a partir de localhost. Para entrar no ar, escolha o Pro a partir de US$ 6/mês para 500 envios, ou pague uma vez: 10.000 envios por US$ 49 ou 50.000 envios por US$ 149, com créditos que nunca expiram. Veja os preços.

  1. Em Allowed domains do formulário, adicione o domínio do seu site e também localhost enquanto desenvolve. Deixe vazio para aceitar envios de qualquer lugar.
  2. Preencha o formulário na sua página e envie. Você cai na sua página de agradecimento, ou na hospedada pelo Ikiform.
  3. Abra Submissions no painel. O novo envio está lá com todos os campos que você mandou.
  4. Envie com o honeypot preenchido, _gotcha=bot. Ele ainda recebe uma resposta de sucesso, mas o envio é guardado como spam e não dispara e-mail nem webhook.
  5. Envie uma mensagem com um link no campo de nome e a frase seo services na mensagem. Ela é guardada como spam com os motivos url_in_name e keyword.
  6. Mude Spam check para Ikiform e envie o formulário HTML simples. Você vê a página de verificação por um instante e depois sua página de agradecimento.
  7. Envie o formulário algumas vezes seguidas. Passado o limite, o formulário responde 429 e o visitante vê a página do Ikiform pedindo para ir mais devagar.
  8. Nada chegou? Mude o filtro de status do painel para Spam. O spam recebe a mesma resposta de sucesso que um envio real.

Solução de problemas

ProblemaO que fazer
O envio está no painel, mas nenhum e-mail ou webhook chegouEle foi guardado como spam. Spam não dispara nada. Abra-o no filtro Spam para ver os motivos e marque-o como não spam para disparar o e-mail e os webhooks.
403 challenge_requiredA verificação de spam do formulário está em Ikiform e um envio com fetch não tinha token. Obtenha um com challenge(formId) de @ikiform/sdk antes de cada envio, ou desative a verificação.
403 origin_not_allowed, ou todo envio cai no spamO domínio da página não está em Allowed domains do formulário. Adicione-o, e também localhost para testes locais. Subdomínios de um domínio listado são permitidos.
429 rate_limitedUm visitante enviou mais do que o limite de taxa do formulário permite. O Standard permite 5 por minuto por visitante. Espere o Retry-After, ou escolha Relaxed ou Custom para formulários que enviam de um IP compartilhado.
403 captcha_failedO token do Turnstile estava ausente, expirado ou foi usado duas vezes. Tokens duram 300 segundos e funcionam uma vez, então renderize um widget novo, ou chame challenge() de novo, após cada tentativa.
O spam continua chegando como recebidoAbra um e veja o que ele tem em comum com os outros. Spam de script pede Spam check em Ikiform; ofertas digitadas pedem um limite de taxa mais rígido e uma revisão semanal.
Visitantes reais dizem que não conseguem enviarConfira se o honeypot está oculto para leitores de tela e com um nome que o preenchimento automático ignora, e se um limite de taxa rígido não está pegando pessoas atrás de um mesmo IP de escritório.

Perguntas

Travou em algum passo ou quer um guia para o seu caso? Envie um e-mail para hello@ikiform.com.