16 Mil Millones de Contraseñas Filtradas: Lo Que los Desarrolladores Deben Hacer Ahora
16 mil millones de credenciales acaban de aparecer en la dark web. La mayoría de los endpoints de login no tienen rate limiting. Aquí está la cadena de ataque exacta y los fixes para detenerla.
Rod
Founder & Developer
Cuando vi el titular — 16 mil millones de credenciales filtradas — fui directo a nuestros datos de escaneo. Hemos analizado cientos de repos en los últimos meses. Y una cosa que aparece consistentemente es endpoints de login sin ningún tipo de protección contra ataques automatizados.
Sin rate limiting. Sin bloqueo de cuenta. Sin detección de brechas. Solo un formulario que acepta cualquier combinación de email y contraseña, sin límite de intentos.
Ese patrón ya era un problema antes de esta filtración. Ahora es una invitación abierta.
La filtración de los 16 mil millones de contraseñas no es una brecha nueva de una empresa. Es un dump agregado — un archivo enorme que consolida docenas de brechas anteriores: RockYou2024, LinkedIn, Adobe, Dropbox, y muchas otras que no tuvieron cobertura mediática. El resultado es aproximadamente 16 mil millones de pares email/contraseña únicos circulando en la dark web. La mayoría a la venta. Una parte disponible gratis en foros de hacking.
El problema no es solo el tamaño. Es la correlación cruzada. Los atacantes ahora pueden vincular la misma dirección de email entre servicios distintos con tasas de éxito mucho más altas. Tu usuario que reutiliza contraseñas — que es la mayoría de tus usuarios, seamos honestos — es el objetivo.
Cómo se ve realmente un ataque de credential stuffing
El credential stuffing no es brute force. No es adivinar contraseñas aleatorias. Es más simple y más efectivo que eso.
La cadena de ataque tiene cuatro pasos:
1. Descargar el dump. Los archivos de credenciales circulan en foros como BreachForums o Telegram. Algunos cuestan unos pocos dólares. Otros son gratuitos.
2. Filtrar por objetivo. Los atacantes filtran el dump por dominio de email relevante para su objetivo, o simplemente usan todos los pares email/contraseña.
3. Correr bots automatizados. Herramientas como OpenBullet o SentryMBA toman el dump y disparan peticiones contra tu endpoint de login a velocidad de máquina — miles por minuto, distribuidas entre múltiples IPs para evadir bloqueos simples.
4. Cosechar cuentas activas. Con una tasa de éxito de apenas 0.1% a 2%, un dump de 16 mil millones de credenciales produce entre 16 millones y 320 millones de cuentas comprometidas. Matemáticas simples con consecuencias enormes.
Lo que hace al stuffing especialmente efectivo es que usa contraseñas reales que usuarios reales eligieron. No genera tráfico obvio de brute force. Si tu endpoint de login no tiene protecciones específicas, un ataque de stuffing se ve casi idéntico al tráfico legítimo.
Por qué tu app es un objetivo aunque tengas 50 usuarios
Hay un error de razonamiento común: "mi app es pequeña, nadie me va a atacar."
Los bots no seleccionan objetivos manualmente. Escanean internet en busca de endpoints de login expuestos y atacan todo lo que encuentran. Tu endpoint /api/auth/login o /api/login es perfectamente legible para cualquier scanner automático.
Además, el valor no siempre está en el volumen. Puede estar en el tipo de cuenta. Un desarrollador que usa la misma contraseña en tu app SaaS y en su cuenta de AWS es un target interesante aunque seas el único usuario activo.
Según el reporte de Verizon Data Breach Investigations 2025, el 44% de las brechas involucraron credenciales robadas o débiles. No exploits sofisticados. Credenciales. El ataque más común no es el más técnico — es el que escala mejor.
Las 5 cosas que implementar esta semana
Ninguna de estas defensas es difícil de implementar. Colectivamente hacen que tu endpoint de login sea un objetivo mucho menos atractivo.
Fix 1 — Rate limiting en el endpoint de login
El rate limiting general de tu API no es suficiente. Tu endpoint de login necesita límites más estrictos, independientes del resto.
// middleware.ts (o tu capa de rate limiting)
// BIEN: límite específico para login, más estricto que el general
import { Ratelimit } from "@upstash/ratelimit"
import { Redis } from "@upstash/redis"
const loginRatelimit = new Ratelimit({
redis: Redis.fromEnv(),
limiter: Ratelimit.slidingWindow(10, "1 m"), // 10 intentos por minuto por IP
analytics: true,
})
export async function POST(req: Request) {
const ip = req.headers.get("x-forwarded-for") ?? "127.0.0.1"
const { success, remaining } = await loginRatelimit.limit(ip)
if (!success) {
return Response.json(
{ error: "Demasiados intentos. Intenta de nuevo en un minuto." },
{
status: 429,
headers: { "Retry-After": "60" },
}
)
}
// ... lógica de autenticación
}El header Retry-After es importante — le dice al cliente cuándo puede volver a intentar, y los clientes bien implementados lo respetan.
Fix 2 — Bloqueo de cuenta tras N fallos consecutivos
El rate limiting por IP tiene una debilidad: los ataques distribuidos. Un atacante con mil IPs distintas puede probar mil contraseñas antes de que cualquier IP alcance el límite.
El bloqueo por cuenta es diferente — bloquea la cuenta objetivo independientemente de cuántas IPs estén probando.
// lib/auth/account-lockout.ts
// BIEN: bloquear la cuenta después de 5 fallos, no solo la IP
async function registrarFalloLogin(email: string): Promise<void> {
const { data: perfil } = await supabase
.from("profiles")
.select("failed_login_attempts, lockout_until")
.eq("email", email)
.single()
const intentosFallidos = (perfil?.failed_login_attempts ?? 0) + 1
const bloqueadaHasta =
intentosFallidos >= 5
? new Date(Date.now() + 15 * 60 * 1000) // 15 minutos
: null
await supabase
.from("profiles")
.update({
failed_login_attempts: intentosFallidos,
lockout_until: bloqueadaHasta,
})
.eq("email", email)
}
async function cuentaBloqueada(email: string): Promise<boolean> {
const { data } = await supabase
.from("profiles")
.select("lockout_until")
.eq("email", email)
.single()
if (!data?.lockout_until) return false
return new Date(data.lockout_until) > new Date()
}Un detalle importante: devuelve el mismo mensaje de error cuando el login falla por contraseña incorrecta y cuando la cuenta está bloqueada. Si das mensajes distintos, le estás diciendo al atacante cuáles emails existen en tu sistema.
Fix 3 — Detección de brechas via Have I Been Pwned
La API de Have I Been Pwned permite verificar si una contraseña aparece en brechas conocidas sin enviar la contraseña. Usa k-Anonymity: enviás los primeros 5 caracteres del hash SHA-1, recibes una lista de sufijos, y comparas localmente. La contraseña nunca sale de tu servidor.
// lib/auth/hibp.ts
// BIEN: verificar si la contraseña aparece en brechas sin exponerla
const verificarContraseñaFiltrada = async (
contraseña: string
): Promise<boolean> => {
const encoder = new TextEncoder()
const hashBuffer = await crypto.subtle.digest(
"SHA-1",
encoder.encode(contraseña)
)
const hashHex = Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("")
.toUpperCase()
const prefijo = hashHex.slice(0, 5)
const sufijo = hashHex.slice(5)
const res = await fetch(`https://api.pwnedpasswords.com/range/${prefijo}`, {
headers: { "Add-Padding": "true" }, // evita timing attacks
})
const texto = await res.text()
// La respuesta es una lista de "SUFIJO:CANTIDAD"
// Si el sufijo de nuestra contraseña aparece, está filtrada
return texto
.split("\n")
.some((linea) => linea.split(":")[0] === sufijo)
}
// Uso en el flujo de registro:
// if (await verificarContraseñaFiltrada(contraseña)) {
// return error("Esta contraseña apareció en brechas conocidas. Elige otra.")
// }Esto no es solo útil para nuevos registros. Podés verificar en el login también — si el usuario se autentica correctamente pero su contraseña está filtrada, es el momento ideal para pedir un cambio.
Fix 4 — MFA en patrones sospechosos
No necesitás forzar MFA en cada login. Forzarlo en patrones de riesgo alto es igual de efectivo y menos irritante para tus usuarios legítimos.
Los patrones que deberían disparar MFA:
- Primer login desde un dispositivo nuevo (comparar fingerprint del dispositivo o user agent)
- Login desde un país distinto al habitual
- Login después de N días de inactividad
- Login que ocurre segundos después de un fallo previo desde otra IP
Si usás Supabase Auth, podés implementar esto en un hook de auth.users o en tu propio middleware verificando el historial de sesiones. Si usás NextAuth, los callbacks de signIn son el lugar correcto.
Fix 5 — Alertas en picos de login fallido
Los primeros cuatro fixes detienen la mayoría de los ataques. Este fix te dice cuándo uno está pasando de todos modos.
Un pico en intentos de login fallidos — especialmente distribuidos entre muchos emails distintos — es la señal más clara de un ataque de stuffing en progreso.
// lib/monitoring/login-alerts.ts
// BIEN: detectar picos anómalos de fallos de login
async function verificarAnomalia(): Promise<void> {
const hace5Minutos = new Date(Date.now() - 5 * 60 * 1000)
const { count } = await supabase
.from("login_attempts")
.select("*", { count: "exact", head: true })
.eq("success", false)
.gte("created_at", hace5Minutos.toISOString())
// Si hay más de 100 fallos en 5 minutos, algo raro está pasando
if (count && count > 100) {
await notificarEquipo({
mensaje: `Pico de fallos de login: ${count} intentos fallidos en 5 minutos`,
nivel: "crítico",
})
}
}El umbral exacto depende del tráfico normal de tu app. Lo importante es tener uno.
La incómoda verdad sobre el código de autenticación generado por IA
Aquí está el patrón que vemos repetidamente en los repos que escaneamos.
Pedís a un asistente de IA que agregue login a tu app. El código que genera maneja el happy path perfectamente: recibe email y contraseña, verifica contra la base de datos, devuelve un token si es correcto. Funciona. Pasa tus pruebas. Va a producción.
Y no tiene ninguna de las cinco protecciones de arriba.
No porque el asistente de IA sea malo — es porque el happy path es lo que pediste. Rate limiting, bloqueo de cuenta, detección de brechas, MFA condicional, alertas de anomalía: ninguna de esas cosas aparece en el código generado a menos que las pidas explícitamente, por nombre, una por una.
Veracode analizó miles de proyectos con código generado por IA en 2025 y encontró que el 45% tenía vulnerabilidades de seguridad — más alto que el promedio del código escrito manualmente. No porque la IA no sepa de seguridad. Porque la IA optimiza para lo que pedís, y la mayoría de los prompts no incluyen requisitos de seguridad.
El problema es que no sabés lo que no sabés. Si nunca pensaste en credential stuffing, no vas a pedirle a la IA que proteja tu endpoint de login contra él.
Es exactamente el tipo de gap que Data Hogo escanea en tu repo — endpoints de autenticación sin rate limiting, ausencia de bloqueo de cuenta, headers de seguridad faltantes. Cosas que son invisibles hasta que alguien te las señala. El escaneo es gratuito y no necesita tarjeta de crédito.
TL;DR
- La filtración de 16 mil millones de contraseñas es un dump agregado de brechas anteriores — credenciales reales de usuarios reales, listas para usarse en ataques automatizados.
- Credential stuffing no es brute force. Usa contraseñas reales con tasas de éxito de 0.1–2%. A escala de 16 mil millones de credenciales, eso es entre 16 y 320 millones de cuentas comprometidas potenciales.
- Tu app es un objetivo aunque sea pequeña. Los bots no discriminan por tamaño.
- Los cinco fixes prioritarios: rate limiting específico en login, bloqueo de cuenta tras N fallos, verificación via HIBP k-Anonymity, MFA en patrones sospechosos, y alertas en picos de fallos.
- El código de autenticación generado por IA maneja el happy path pero omite estas protecciones a menos que las pidas explícitamente.
- Ningún fix es complejo. Todos los ejemplos de arriba son menos de 30 líneas. El problema no es la dificultad técnica — es saber que hay que hacerlo.