Ataque: Email bombing 🎯 Límite de peticiones, bloqueo de IPs, validaciones
Tiempo de lectura: 6.49 minutos
Lo que encontrarás en este artículo
Si tienes un proyecto web y de pronto empiezas a recibir muchos requests para un endpoint específico, entonces puedes estar siendo víctima de un ataque.
¿Cómo se llama dicho ataque?
Depende del objetivo y del modo en que se haga:
-
HTTP flood / DoS (o DDoS si viene de muchos hosts) — si el script manda muchas peticiones para saturar recursos.
-
Abuso de API / scraping — si automatiza llamadas para robar datos o abusar de cuota.
-
Brute-force / credential stuffing — si prueba muchas combinaciones de usuario/clave.
-
Fuzzing / scanning — si envía cargas/inputs aleatorios para encontrar vulnerabilidades.
-
Exploit script — si el script envía payloads para aprovechar una vulnerabilidad concreta (SQLi, RCE, LFI, etc).
A fin de evitar esto, podemos usar/habilitar lo siguiente:
- WAF (Web Application Firewall).
- Autenticación / validación de requests.
- Rate limiting.
- Captcha en puntos sensibles (login, forms).
- Bloqueo automático por patrones (fail2ban / Cloudflare rules).
- Validación y saneamiento para evitar explotaciones (SQLi, XSS).
- Logs y alertas (requests por IP, user-agent, ratio).
Caso de ejemplo
Hoy vamos a analizar un caso bien particular.
Estaba navegando y de pronto recibí este correo:
Aquí se advierte que se intentó insertar registros, pero falló, ya que contenían textos muy extensos.
Curioso, ¿no?
Así mismo me encontré con esto en el inbox de [email protected]:
Estos mensajes los envía Amazon SES cada vez que falla al enviar un correo.
Es usual ver uno o que otro al momento de enviar campañas de correo.
Pero esta vez, ninguna campaña se encontraba activa y aún así, cada segundo continuaban llegando más y más de estas notificaciones.
Significado: Algo o alguien está provocando que se envíen muchos correos.
En este caso el ataque se conoce como email abuse o email bombing.
También conocido como form abuse.
Investigación general
Como este sitio trabaja con CloudFlare, lo primero que hice fue visitar la sección de Analytics.
Y revisar la cantidad de requests recibidos en las últimas 24 horas.
Desde el siguiente gráfico ya se observa tráfico inusual:
- En la parte inferior (en el eje X) tenemos las últimas 24 horas.
- En el eje Y tenemos la cantidad de requests.
Nota: Puedes abrir la imagen en otra pestaña para verla a tamaño completo.
Como se observa:
- Cada minuto usualmente tenemos menos de 500 requests.
- Pero en un momento determinado, hay un pico de 6 mil requests.
Para analizar tráfico inusual es recomendable revisar Sampled logs:
Esto muestra ejemplos de requests.
Y desde un primer momento vemos un patrón:
- Todos los últimos requests provienen de la misma IP.
- Todos son requests al endpoint `/register/ (la ruta de registro).
A fin de tener una mejor visión global, podemos revisar la sección de Top Statistics:
Esto nos indica desde dónde proviene la mayor cantidad de visitas.
Por ejemplo:
- Desde qué IP.
- Desde qué tipo de dispositivo.
- Desde qué sistema operativo.
- Desde qué navegador.
Y en este caso en particular hemos encontrado a la IP ofensiva.
Lo curioso, y bueno de esto, es que el ataque sucedió casi al mismo tiempo en que me conecté a la PC.
En un primer momento los requests se veían de esta manera:
Y al poco tiempo, subieron de 9.26k a 19.25k como se observa en la imagen anterior.
Investigación del atacante
En CloudFlare tenemos una opción llamada Security Center, que nos permite indagar más, desde su sección Investigate:
Por ejemplo, podemos ingresar la IP del atacante y obtener más información:
Solución
Podemos actuar de muchas formas.
Veamos las que se aplicaron en este caso.
Rate Limiting
Lo primero fue limitar la cantidad de registros (o intentos de registro) que puede hacer cada IP.
¿Por qué alguien intentaría registrar miles de usuarios?
Como es un escenario atípico, este límite no afecta a usuarios legítimos.
A esta práctica de limitar la cantidad de requests permitidos se conoce como rate limiting, cuya traducción literal sería límite de frecuencia o limitación de tasa.
En Laravel esta implementación es muy sencilla:
// Permite hasta 5 requests por hora por IP
RateLimiter::for('register', function (Request $request) {
return Limit::perHour(5)
->by($request->ip())
->response(function (Request $request, array $headers) {
return response()->json([
'message' => 'Demasiados intentos de registro. Por favor intenta de nuevo más tarde.'
], 429, $headers);
});
});
// Permite hasta 5 requests por minuto por IP
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)
->by($request->ip())
->response(function (Request $request, array $headers) {
return response()->json([
'message' => 'Demasiados intentos de inicio de sesión. Por favor intenta de nuevo en unos minutos.'
], 429, $headers);
});
});
El código anterior declara límites.
El middleware llamado throttle nos permite aplicar tales límites a nuestras rutas.
Route::post('register', [RegisterController::class, 'store'])
->middleware('throttle:register');
Route::post('login', [LoginController::class, 'store'])
->middleware('throttle:login');
Security Rules
La práctica anterior detiene el efecto del ataque.
Antes el atacante podía hacer requests continuamente al endpoint de registro, y conseguir que el servidor lo atendiera.
Ahora nuestro proyecto Laravel le respondería con un mensaje de error al exceder el límite.
Sin embargo, aunque los requests ya no tenían alguno, continuaban sucediendo.
Esto significa que hemos validado correctamente a nivel de aplicación, pero aún podemos hacer algo más.
La idea es evitar que la IP ofensiva continúe realizando requests a nuestro servidor.
Para esto nos dirigmos a la sección de Security Rules:
Y definimos una nueva regla, a fin de excluir dicha IP:
Básicamente definimos lo siguiente:
- Cuando: la IP es igual a esta.
- Acción: Bloquear.
Guardamos cambios y volvemos a revisar nuestros requests:
Aquí podemos ver que todos los requests entrantes son atendidos por el servidor.
Pero luego de definir esta regla, vemos que los requests empiezan a ser mitigados por CloudFlare:
Y así luce el gráfico de requests en las últimas 24 horas:
Empieza a aparecer una línea naranja representando a los requests mitigados.
Significado: Estos requests no son enviados a nuestro servidor, quitándole carga.
El resumen de 24 horas muestra cómo la cantidad de requests mitigados va en aumento:
Al revisar las últimas 6 horas, confirmamos que el ataque es reciente:
Y así mismo vemos cómo ahora tenemos más requests mitigados que requests atendidos por nuestro servidor.
En este punto, el servidor sólo atiende requests legítimos.
Objetivo del ataque
Si revisamos un fragmento del error reportado por Papertrail, encontramos lo siguiente:
Oct 18 12:41:57 lamp-pym-nyc3 php: [2025-10-18 12:41:57] production.ERROR: SQLSTATE[22001]: String data, right truncated: 1406 Data too long for column 'name' at row 1 (Connection: mysql, SQL: insert into `users` (`name`, `email`, `password`, `confirmation_code`, `updated_at`, `created_at`) values (✅ We’ve partnered with Binance. Instead of the iPhone 16 Pro Max, you have got an account with 1.3444 BTC - https://***.org/BINANCE-10-17-10?d7zn - After withdrawing the funds, please send proof of the prize to the Binance online support chat ✅, [email protected], ***, 9i0WqiypGouXx540Ic6UghYRW, 2025-10-18 12:41:57, 2025-10-18 12:41:57))
El atacante intentó registrar muchos usuarios con un nombre bien extenso, al punto que producía un error al ejecutar el INSERT en la base de datos.
El mensaje está en inglés. Traducido al español dice que nos hemos asociado con Binance, y que te estamos otorgando una cuenta con 1.3444 BTC.
Lo más probable es que busquen que se visite la página promocionada, con algún engaño o estafa.
Por seguridad he omitido el dominio.
La idea de usar el registro es que, por cada usuario nuevo, nosotros enviamos un correo de confirmación.
Imagina que el correo dice:
Hola Juan, por favor confirma tu correo.
Ahora reemplaza Juan por el texto mencionado previamente.
Ese era el objetivo del atacante.
Como el texto era muy extenso, fallaba. Y de seguro no sabía por qué.
Pero en un momento determinado, lo adaptó para que el sistema lo aceptara.
Una vez frenado el ataque, y comprendido cuál fue el objetivo, ¿qué más podemos hacer?
Analizando el impacto
Primero veamos cuántos usuarios han sido insertados en la base de datos.
select count(1)
from users
where id >= 25734 and name like '%https://%' ;
Esto nos indica que 1227 "usuarios" fueron creados por el atacante.
De ellos, 4 usuarios confirmaron su registro:
select *
from users
where id >= 25734 and name like '%https://%'
and confirmed = 1;
Como ellos no son usuarios legítimos de la plataforma, lo mejor es eliminar sus cuentas:
delete from users
where id >= 25734 and name like '%https://%';
El hecho de que el delete funcionara demuestra que no han navegado por el sitio.
Además de estos "falsos usuarios" veamos si históricamente se han creado usuarios conteniendo una URL en el nombre:
select * from users where name like '%https://%';
Esto demuestra que ninguno de los usuarios existentes tenía este patrón.
Tampoco será posible de ahora en adelante, gracias a los siguientes ajustes.
Posterior a la solución
Implementemos un par de detalles más, para estar preparados por si se intenta algo similar en el futuro.
Min y Max
Primero, revisemos cuál es la longitud máxima que tiene el correo de nuestros usuarios.
La idea es validar a nivel de aplicación sin siquiera alcanzar la base de datos.
Por ejemplo, ejecutamos la siguiente query:
SELECT MAX(LENGTH(email)) AS max_email_length
FROM users;
El resultado es 53.
Entonces con limitar hasta 60 caracteres el correo me parece razonable.
Tan sólo @gmail.com tiene 10 caracteres.
Por tanto, exigir un mínimo de 12 me parece también justo.
El ataque en este caso fue contra el nombre, pero no quisiera limitar la longitud, porque realmente existen nombres bien largos.
RegEx
Lo que sí podemos hacer es usar una expresión regular.
Como la siguiente:
'regex:/^[\pL\s\-_.]+$/u'
La idea es evitar que se agreguen direcciones URL en la columna name.
Expliación de la regular expression anterior:
| Parte | Significado |
|---|---|
| /^ | Representa el inicio del string |
| [...] | Hace match con cualquier 1 caracter en su interior |
| \pL | Cualquier letra de cualquier lenguaje (Incluyendo el Español: á, é, í, ó, ú, ñ, etc) |
| \s | Espacios en blanco, tabs (permite dejar espacios entre los nombres) |
| - | Guión (permite "García-López") |
| _ | Guión bajo (permite "John_Smith") |
| . | Punto (permite "Dr. Smith" o iniciales) |
| + | Uno o más caracteres deben encontrarse de la expresión entre corchetes |
| $/ | Representa el final del string |
| u | Modo Unicode (permite tildes, emojis, etc) |
La idea es validar los nombres de nuestros usuarios aplicando esta regEx.
Ejemplos de lo que está permitido ✅
"María García" // ✅ Tildes
"Jean-Pierre" // ✅ Guiones
"Dr. Smith" // ✅ Puntos
"José_Luis" // ✅ Guiones bajos
"李明" // ✅ Caracteres chinos
"Владимир" // ✅ Caracteres cirílicos
"محمد" // ✅ Caracteres árabes
Ejemplos que no pasan la validación ❌
"John123" // ❌ Números
"test@email" // ❌ Símbolos
"<script>" // ❌ Caracteres especiales / HTML
"user!name" // ❌ Exclamación
"name#tag" // ❌ Hashtag
"hola😀" // ❌ Emojis
Conclusión
Muchas veces pasamos por alto validaciones que asumimos obvias o muy básicas.
Validar es muy sencillo, y aún así a veces nos preguntamos:
¿Por qué validar la longitud mínima y máxima?
Por más simples que parezcan, como hemos visto hoy, reducen significativamente los vectores de ataque.
Si deseas continuar aprendiendo, te invito a visitar mis cursos.
¡Gracias por leer hasta el final!