Acabe com o spam no formulário de contato

Acabe com o spam no formulário de contato6 min de leitura

Como limitar a taxa de envios de formulário

Limite a taxa de envios por visitante com um armazenamento compartilhado, responda 429 com Retry-After e evite bloquear escritórios que compartilham um IP.

Preet SutharFundador, Ikiform

Conte os envios por IP do visitante num armazenamento que todas as instâncias do servidor compartilham, como o Redis, permita uma pequena rajada e responda 429 com um cabeçalho Retry-After quando a contagem acabar. Um contador em memória só funciona num servidor único de longa duração, nunca em hosts serverless. Defina o limite pelo jeito como pessoas reais usam o formulário e deixe folga para escritórios e eventos em que muita gente compartilha um IP.

O que os limites de taxa têm de diferente

Um limite de taxa não distingue spam de mensagem legítima. Ele barra volume: um script enviando em loop, um bot circulando pelo seu formulário ou uma pessoa martelando o botão. Isso o torna a camada mais barata, já que roda antes de você ler o corpo, e a mais bruta, já que não distingue um atacante de um escritório movimentado. Três coisas decidem se ele ajuda ou atrapalha: onde fica a contagem, pelo que você conta e com que rapidez a cota volta.

Uma rajada de envios de um visitante contra um limite suave por visitanteenvios 1 a 3em 10 segundos:aceitosenvio 4429 comRetry-Aftera cada 12 smais um enviopermitidoum minuto desilênciocota completade novorajadaesperarecargaUma rajada de envios de um visitante contra um limite suave por visitanteenvios 1 a 3em 10 segundos: aceitosenvio 4429 com Retry-Aftera cada 12 smais um envio permitidoum minuto de silênciocota completa de novorajadaesperarecarga
  1. Um visitante manda 3 envios em 10 segundos. Todos são aceitos.
  2. O seguinte é recusado com 429 e um cabeçalho Retry-After dizendo quanto esperar.
  3. Depois disso, mais um envio é permitido a cada 12 segundos.
  4. Após um minuto inteiro de silêncio, a cota volta por completo.
Estes são os números do Standard do Ikiform: 5 por minuto, no máximo 3 a cada 10 segundos. Uma recarga suave significa que ninguém espera uma janela inteira por mais um envio.

Onde fica a contagem

ArmazenamentoFunciona emCuidado com
Um Map em memóriaUm servidor único de longa duraçãoSome ao reiniciar. Cada instância serverless tem seu próprio map, então o limite real é desconhecido
Redis, como o UpstashQualquer lugar, inclusive serverless e edgeUma chamada de rede por envio. Escolha uma região perto do seu servidor
Binding de rate limiting do Cloudflare WorkersWorkersAproximado e contado por localidade da Cloudflare, só com períodos de 10 ou 60 segundos
Um proxy reverso, como o limit_req do nginxServidores atrás do seu próprio proxyConta por proxy. Precisa do IP real do cliente

O algoritmo importa menos que o armazenamento, mas molda o que os visitantes sentem. Uma janela fixa zera na virada do minuto, então um script pode mandar uma cota inteira no fim de um minuto e outra no começo do seguinte. Uma janela deslizante suaviza essa borda pesando a janela anterior. Um token bucket, ou o algoritmo parecido que o Ikiform usa, recarrega um envio por vez, o que deixa quem cometeu um erro de digitação enviar de novo poucos segundos depois sem abrir a porta para uma rajada. Para um formulário de contato, qualquer um deles funciona se os números combinarem com o uso real.

Os exemplos abaixo usam o @upstash/ratelimit do Upstash, porque funciona igual em todo framework e em hosts serverless. Defina UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN e salve o helper uma vez:

lib/limit.ts
import { Ratelimit } from "@upstash/ratelimit";
import { Redis } from "@upstash/redis";

const contact = new Ratelimit({
  redis: Redis.fromEnv(),
  limiter: Ratelimit.slidingWindow(5, "60 s"),
  prefix: "contact",
});

export async function allow(ip: string | null | undefined) {
  const { success, reset } = await contact.limit(ip || "no-ip");
  return { ok: success, retryAfter: Math.max(1, Math.ceil((reset - Date.now()) / 1000)) };
}

reset é um horário Unix em milissegundos, então o helper o converte em segundos inteiros para o Retry-After. Envios sem IP compartilham um único bucket, o que impede que um cabeçalho ausente vire passe livre.

Aplique no seu framework

Um formulário HTML simples não tem servidor para contar nada. Envie-o ao Ikiform e escolha um preset no formulário; um visitante acima do limite vê uma breve página pedindo para ir mais devagar em vez de um erro.

contact.html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="POST">
  <label for="email">Email</label>
  <input id="email" name="email" type="email" required />
  <label for="message">Message</label>
  <textarea id="message" name="message" required></textarea>
  <button>Send</button>
</form>

No Cloudflare Workers, o binding embutido é a opção mais barata:

wrangler.toml
[[ratelimits]]
name = "CONTACT_LIMIT"
namespace_id = "1001"

  [ratelimits.simple]
  limit = 5
  period = 60
src/index.ts
const ip = request.headers.get("CF-Connecting-IP") ?? "no-ip";
const { success } = await env.CONTACT_LIMIT.limit({ key: `contact:${ip}` });
if (!success) return new Response("Too many messages", { status: 429, headers: { "Retry-After": "60" } });

A Cloudflare documenta esse binding como aproximado e contado separadamente em cada localidade, então um ataque espalhado pode ultrapassá-lo. Para um limite exato entre localidades, a Cloudflare recomenda um Durable Object. Atrás do seu próprio nginx, o limit_req faz a contagem antes de sua aplicação ver o envio:

nginx.conf
limit_req_zone $binary_remote_addr zone=contact:10m rate=5r/m;

server {
  location = /api/contact {
    limit_req zone=contact burst=3 nodelay;
    limit_req_status 429;
    proxy_pass http://127.0.0.1:3000;
  }
}

Pelo que contar

  • IP e formulário juntos. A escolha usual. Um visitante não consegue inundar este formulário, e usar outro formulário do seu site não gasta a mesma cota.
  • Muitas pessoas, um IP. Escritórios, escolas, operadoras de celular e Wi-Fi de conferência colocam muita gente atrás de um endereço. Mantenha o limite por IP folgado o bastante para elas, ou aumente-o em dias de evento.
  • O IP real do cliente. Atrás de um proxy ou CDN, leia o cabeçalho que seu host define e confie só nele. Um valor bruto de X-Forwarded-For pode ser definido pelo cliente.
  • O IP do seu próprio servidor. Se seu servidor repassa envios a outro serviço, todo visitante compartilha o IP do seu servidor lá. Limite do seu lado, pelo IP do visitante, antes de repassar.

Os limites de taxa do Ikiform

O Ikiform conta cada envio com exatidão, por IP do visitante e formulário, antes de ler o corpo, com recarga suave em vez de janelas fixas. Cada formulário escolhe um preset em Rate limit. Uma proteção contra enxurradas de 600 por minuto por IP em todos os formulários roda antes de qualquer um deles.

PresetPor visitanteIndicado para
Standard5 por minuto, no máximo 3 a cada 10 segundosA maioria dos formulários
Relaxed20 por minutoEventos em Wi-Fi compartilhado, formulários atrás de login
Strict2 a cada 10 minutos, mais um a cada 5 minutosFormulários de contato e de vendas
CustomDe 1 a 1.000 por 10 segundos, minuto, 10 minutos, hora ou diaUma inscrição por pessoa, quiosques

O formulário inteiro também aceita 300 por minuto somando todos os visitantes, a menos que você defina um teto Custom. Acima do limite, um envio pelo navegador vê uma página pedindo para ir mais devagar e o fetch recebe 429 rate_limited com Retry-After, que o Ikiform expõe via CORS para o código acima poder lê-lo. A documentação de limites de taxa explica a recarga.

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.

Confira se funciona

  1. Envie o formulário várias vezes seguidas. Passado o limite, você recebe 429 com um cabeçalho Retry-After, ou a página do Ikiform pedindo para ir mais devagar.
  2. Espere os segundos indicados em Retry-After e envie de novo. O envio é aceito.
  3. Envie de uma segunda rede, como seu celular fora do Wi-Fi. Ela tem sua própria cota.
  4. Se seu servidor repassa envios, envie rapidamente de dois aparelhos e confira que nenhum compartilha a contagem do outro.

Pegadinhas

  • Envios recusados não devem gastar cota. Conte antes do trabalho e não deixe um envio recusado empurrar a janela para a frente, senão um visitante ansioso se tranca do lado de fora.
  • Não limite o carregamento da página. Conte envios, não visualizações do formulário, senão uma pessoa recarregando gasta seus envios.
  • 429 rate_limited Um 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.
  • IPv6. Uma pessoa pode ter muitos endereços IPv6. Para um limite rígido, conte pelo prefixo /64 em vez do endereço completo.

Perguntas

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