Guia practica para asegurar los registros SaaS B2B
El mes pasado, un cliente SaaS B2B acudio a nosotros con un problema familiar: los registros parecian normales en la superficie, pero los creditos gratuitos se agotaban rapido. Al analizar los logs descubrimos un ataque coordinado — decenas de registros desde los mismos subredes /24, emails desechables en rotacion y un atacante reutilizando infraestructura en multiples cuentas falsas.
Este articulo recorre la stack de defensa que recomendamos. Los nombres e identificadores estan anonimizados, pero el flujo esta listo para produccion.
Que facilito el ataque
Tres factores jugaron a favor del atacante:
- Todos los creditos gratuitos otorgados al registrarse — sin friccion, sin verificacion, valor inmediato.
- Sin vinculacion de dispositivo — la misma huella del navegador podia registrarse una y otra vez.
- Comprobaciones IP omitidas o mal configuradas — detras del proxy de Cloudflare (nube naranja), la app leia las IPs del proxy en lugar de la direccion real del visitante.
Asegurar los registros no se trata de una bala de plata, sino de superponer senales que se refuerzan mutuamente.
Prioridad 1: leer la IP real
Si tu app esta detras de Cloudflare con el proxy activado (nube naranja), req.ip o X-Forwarded-For por si solo no bastan. Cloudflare envia la IP real del visitante en el 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 control posterior — rate limits, reputacion IP, bloqueo — depende de acertar en este primer paso.
Prioridad 2: puntuar la IP en el registro
Ejecuta la API Veille IP Reputation de forma asincrona durante el registro. Si puedes evitarlo, no bloquees la respuesta HTTP; encola la comprobacion y actua en 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();
}
El threat_score agrega las senales detectadas:
| Score | Significado |
|---|---|
| 0 | IP residencial o empresarial limpia |
| 1–25 | Senales menores (datacenter, VPN) |
| 25–50 | Riesgo moderado (multiples senales) |
| 50+ | Riesgo alto (Tor, abusador conocido, proxy) — bloquear o exigir verificacion extra |
En los logs de nuestro cliente, las IPs atacantes se agrupaban en rangos /24 con scores superiores a 50. Bloquear la subred tras abusos repetidos, no solo la IP aislada, redujo el volumen rapidamente.
Prioridad 3: integrar FingerprintJS
Instala FingerprintJS en tus paginas de registro e inicio de sesion. Guarda el visitorId en cada intento.
import FingerprintJS from "@fingerprintjs/fingerprintjs";
async function getFingerprint(): Promise<string> {
const fp = await FingerprintJS.load();
const result = await fp.get();
return result.visitorId;
}
Luego aplica rate limits tanto en IP como en huella:
- Tras 5 intentos fallidos de registro o login desde la misma huella o IP → bloqueo 24 horas.
- Registra ambos valores en cada intento para pivotar facilmente en el back office.
Prioridad 4: validar el email de forma asincrona
Los emails desechables no detendran a un atacante determinado, pero filtran gran parte del abuso de bajo esfuerzo. Llama a la API Veille Email Validation en paralelo con la comprobacion 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();
}
// En el handler de registro:
const [ipResult, emailResult] = await Promise.all([
scoreIp(clientIp),
validateEmail(email),
]);
if (emailResult.disposable || emailResult.risk_score > 70) {
await blockIpTemporarily(clientIp, "disposable_email");
return { error: "Registro bloqueado" };
}
if (ipResult.threat_score >= 50) {
await blockIpTemporarily(clientIp, "high_threat_ip");
return { error: "Registro bloqueado" };
}
Ejecuta estas comprobaciones despues de recibir el formulario pero antes de otorgar creditos. Si alguna senal falla, bloquea la IP durante una ventana configurable y devuelve un error generico.
Prioridad 5: vincular la huella a la verificacion de email
Crear una cuenta no es lo mismo que demostrar que controlas la bandeja. Exige verificacion de email antes de que los creditos sean utilizables.
El paso que la mayoria de equipos omite: la huella al registrarse debe coincidir con la huella al hacer clic en el enlace de verificacion.
// Al registrarse — guardar verificacion pendiente
await db.pendingVerifications.create({
email,
fingerprintSignup: fingerprint,
ipSignup: clientIp,
token: verificationToken,
expiresAt: addHours(new Date(), 24),
});
// Al verificar — comparar huella
const pending = await db.pendingVerifications.findByToken(token);
if (pending.fingerprintSignup !== currentFingerprint) {
await flagAccount(pending.email, "fingerprint_mismatch");
return { error: "Verificacion fallida" };
}
await activateAccount(pending.email);
Una discrepancia no siempre significa fraude — los usuarios cambian de dispositivo — pero discrepancias repetidas desde IPs de alto riesgo son una senal fuerte.
Prioridad 6: dejar de regalar todos los creditos al registrarse
Los creditos gratuitos son el ROI del atacante. Reparte el valor en acciones dificiles de automatizar:
| Hito | Creditos | Por que |
|---|---|---|
| Cuenta creada | 0 | Sin valor inmediato que extraer |
| Email verificado | +10 | Demuestra control de la bandeja |
| Onboarding completado | +25 | Requiere interaccion real con el producto |
| Compartido en redes | +15 | Anade friccion y trazabilidad |
Ejemplo de flags en el 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;
}
Los bots optimizan el camino mas corto hacia los creditos. Un onboarding con llamadas API reales, elecciones de configuracion o un tutorial breve rompe los flujos scriptados.
Vision general del flujo de registro
El usuario envia el formulario
│
▼
Leer CF-Connecting-IP
│
▼
Comprobar rate limits huella + IP ──► bloqueado? → 429, log
│
▼
Crear cuenta (0 creditos)
│
├──► async: reputacion IP
└──► async: validacion email
│
▼
amenaza detectada? → bloquear IP, marcar cuenta
│
▼
Enviar email de verificacion (guardar huella del registro)
│
▼
Clic en el enlace → huella coincide? → +10 creditos
│
▼
Onboarding completado → +25 creditos
│
▼
Compartido en redes confirmado → +15 creditos
Booleanos utiles en el back office
Da a tu equipo toggles para reaccionar sin desplegar codigo:
block_disposable_emails— rechazar bandejas desechables al registrarseblock_high_threat_ips— bloquear auto IPs conthreat_score >= 50require_fingerprint_match— exigir coincidencia de huella en la verificacion emailrate_limit_by_fingerprint— activar throttling por huellagrant_credits_on_signup— mantener enfalsedurante un ataque activo
Estos flags permiten reforzar las defensas durante un incidente y relajarlas cuando el trafico se normaliza.
Ultimo recurso: ralentizar sin alertar
Si una IP es claramente abusiva pero quieres evitar alertar al atacante, algunos equipos introducen friccion artificial — respuestas retardadas, CAPTCHA en el segundo intento o un codigo SMS falso que nunca llega. Es controvertido, debe reservarse como ultimo recurso, quedar claramente registrado internamente y nunca mostrarse a usuarios legitimos.
El objetivo es quemar el tiempo del atacante, no el tuyo.
Resultados
Tras implementar esta stack — extraccion IP real, comprobaciones Veille IP y email, rate limits FingerprintJS, verificacion vinculada a la huella y creditos progresivos — el volumen de registros abusivos del cliente cayo bruscamente en 48 horas. Los usuarios legitimos completaron el onboarding sin notar los controles adicionales.