Guia pratico para proteger cadastros SaaS B2B
No mes passado, um cliente SaaS B2B veio ate nos com um problema familiar: os cadastros pareciam normais na superficie, mas os creditos gratuitos sumiam rapido. Ao analisar os logs, descobrimos um ataque coordenado — dezenas de registros dos mesmos sub-redes /24, emails descartaveis em rotacao e um atacante reutilizando infraestrutura em multiplas contas falsas.
Este artigo percorre a stack de defesa que recomendamos. Nomes e identificadores estao anonimizados, mas o fluxo esta pronto para producao.
O que facilitou o ataque
Tres fatores jogaram a favor do atacante:
- Todos os creditos gratuitos concedidos no cadastro — sem friccao, sem verificacao, valor imediato.
- Sem vinculacao de dispositivo — a mesma impressao digital do navegador podia se cadastrar repetidamente.
- Verificacoes de IP ausentes ou mal configuradas — atras do proxy Cloudflare (nuvem laranja), o app lia IPs de proxy em vez do endereco real do visitante.
Proteger cadastros nao e uma bala de prata — e empilhar sinais que se reforcam mutuamente.
Prioridade 1: ler o IP real
Se seu app esta atras do Cloudflare com proxy ativado (nuvem laranja), req.ip ou X-Forwarded-For sozinhos nao bastam. O Cloudflare envia o IP real do visitante no header CF-Connecting-IP.
function getClientIp(request: Request): string {
const cfIp = request.headers.get("cf-connecting-ip");
if (cfIp) return cfIp;
const forwarded = request.headers.get("x-forwarded-for");
if (forwarded) return forwarded.split(",")[0].trim();
return "0.0.0.0";
}
Cada verificacao posterior — rate limits, reputacao de IP, bloqueio — depende de acertar este primeiro passo.
Prioridade 2: pontuar o IP no cadastro
Execute a API Veille IP Reputation de forma assincrona durante o cadastro. Se possivel, nao bloqueie a resposta HTTP nela; enfileire a verificacao e aja em segundos.
const API_KEY = process.env.VEILLE_API_KEY!;
const BASE_URL = "https://api.veille.io/v1";
async function scoreIp(ip: string) {
const response = await fetch(
`${BASE_URL}/intelligence/ip?query=${encodeURIComponent(ip)}`,
{ headers: { "x-api-key": API_KEY } }
);
return response.json();
}
O threat_score agrega os sinais detectados:
| Score | Significado |
|---|---|
| 0 | IP residencial ou empresarial limpa |
| 1–25 | Sinais menores (datacenter, VPN) |
| 25–50 | Risco moderado (multiplos sinais) |
| 50+ | Risco alto (Tor, abusador conhecido, proxy) — bloquear ou exigir verificacao extra |
Nos logs do nosso cliente, IPs atacantes se agrupavam em faixas /24 com scores acima de 50. Bloquear a sub-rede apos abusos repetidos, nao apenas o IP isolado, reduziu o volume rapidamente.
Prioridade 3: integrar FingerprintJS
Instale FingerprintJS nas paginas de cadastro e login. Armazene o visitorId em cada tentativa.
import FingerprintJS from "@fingerprintjs/fingerprintjs";
async function getFingerprint(): Promise<string> {
const fp = await FingerprintJS.load();
const result = await fp.get();
return result.visitorId;
}
Depois aplique rate limits tanto em IP quanto em impressao digital:
- Apos 5 tentativas falhas de cadastro ou login do mesmo fingerprint ou IP → bloqueio por 24 horas.
- Registre ambos os valores em cada tentativa para pivotar facilmente no back office.
Prioridade 4: validar o email de forma assincrona
Emails descartaveis nao param um atacante determinado, mas filtram grande parte do abuso de baixo esforco. Chame a API Veille Email Validation em paralelo com a verificacao de IP:
async function validateEmail(email: string) {
const response = await fetch(
`${BASE_URL}/intelligence/email?query=${encodeURIComponent(email)}`,
{ headers: { "x-api-key": API_KEY } }
);
return response.json();
}
// No handler de cadastro:
const [ipResult, emailResult] = await Promise.all([
scoreIp(clientIp),
validateEmail(email),
]);
if (emailResult.disposable || emailResult.risk_score > 70) {
await blockIpTemporarily(clientIp, "disposable_email");
return { error: "Cadastro bloqueado" };
}
if (ipResult.threat_score >= 50) {
await blockIpTemporarily(clientIp, "high_threat_ip");
return { error: "Cadastro bloqueado" };
}
Execute essas verificacoes depois de receber o formulario mas antes de conceder creditos. Se algum sinal falhar, bloqueie o IP por uma janela configuravel e retorne um erro generico.
Prioridade 5: vincular fingerprint a verificacao de email
Criar uma conta nao e o mesmo que provar que voce controla a caixa de entrada. Exija verificacao de email antes que os creditos sejam utilizaveis.
A etapa critica que a maioria das equipes omite: o fingerprint no cadastro deve corresponder ao fingerprint ao clicar no link de verificacao.
// No cadastro — armazenar verificacao pendente
await db.pendingVerifications.create({
email,
fingerprintSignup: fingerprint,
ipSignup: clientIp,
token: verificationToken,
expiresAt: addHours(new Date(), 24),
});
// Na verificacao — comparar fingerprint
const pending = await db.pendingVerifications.findByToken(token);
if (pending.fingerprintSignup !== currentFingerprint) {
await flagAccount(pending.email, "fingerprint_mismatch");
return { error: "Verificacao falhou" };
}
await activateAccount(pending.email);
Uma divergencia nem sempre significa fraude — usuarios trocam de dispositivo — mas divergencias repetidas de IPs de alto risco sao um sinal forte.
Prioridade 6: parar de dar todos os creditos no cadastro
Creditos gratuitos sao o ROI do atacante. Distribua valor em acoes dificeis de automatizar:
| Marco | Creditos | Por que |
|---|---|---|
| Conta criada | 0 | Sem valor imediato a extrair |
| Email verificado | +10 | Prova controle da caixa de entrada |
| Onboarding concluido | +25 | Exige interacao real com o produto |
| Compartilhamento social confirmado | +15 | Adiciona friccao e trilha de auditoria |
Exemplo de flags no back office:
interface UserCredits {
emailVerified: boolean; // +10 creditos
onboardingCompleted: boolean; // +25 creditos
sharedOnSocial: boolean; // +15 creditos
}
function getAvailableCredits(user: UserCredits): number {
let credits = 0;
if (user.emailVerified) credits += 10;
if (user.onboardingCompleted) credits += 25;
if (user.sharedOnSocial) credits += 15;
return credits;
}
Bots otimizam o caminho mais rapido para creditos. Um onboarding com chamadas API reais, escolhas de configuracao ou um tutorial curto quebra fluxos scriptados.
Visao geral do fluxo de cadastro
Usuario envia formulario
│
▼
Ler CF-Connecting-IP
│
▼
Verificar rate limits fingerprint + IP ──► bloqueado? → 429, log
│
▼
Criar conta (0 creditos)
│
├──► async: reputacao IP
└──► async: validacao email
│
▼
ameaca detectada? → bloquear IP, marcar conta
│
▼
Enviar email de verificacao (armazenar fingerprint do cadastro)
│
▼
Clique no link → fingerprint coincide? → +10 creditos
│
▼
Onboarding concluido → +25 creditos
│
▼
Compartilhamento social confirmado → +15 creditos
Booleanos uteis no back office
De a sua equipe toggles para reagir sem deploy:
block_disposable_emails— rejeitar caixas descartaveis no cadastroblock_high_threat_ips— bloquear auto IPs comthreat_score >= 50require_fingerprint_match— exigir correspondencia de fingerprint na verificacao de emailrate_limit_by_fingerprint— ativar throttling por fingerprintgrant_credits_on_signup— manter emfalsedurante ataque ativo
Esses flags permitem reforcar defesas durante um incidente e relaxa-las quando o trafego normaliza.
Ultimo recurso: desacelerar sem alertar
Se um IP e claramente abusivo mas voce quer evitar alertar o atacante, algumas equipes introduzem friccao artificial — respostas atrasadas, CAPTCHA na segunda tentativa ou um codigo SMS falso que nunca chega. E controverso, deve ser ultimo recurso, claramente registrado internamente e nunca mostrado a usuarios legitimos.
O objetivo e queimar o tempo do atacante, nao o seu.
Resultados
Apos implementar esta stack — extracao IP real, verificacoes Veille IP e email, rate limits FingerprintJS, verificacao vinculada ao fingerprint e creditos progressivos — o volume de cadastros abusivos do cliente caiu bruscamente em 48 horas. Usuarios legitimos concluiram o onboarding sem notar os controles adicionais.