Cómo evitar el bombardeo de suscripciones en formularios
Un atacante envía una dirección a cientos de formularios. El opt-in confirmado y los límites de tasa evitan que tu servidor envíe uno de esos mensajes.
¿Qué es el bombardeo de suscripciones?
El bombardeo de suscripciones es un ataque que utiliza el formulario de registro para saturar la bandeja de entrada de otra persona. El atacante toma la dirección de correo electrónico de una víctima y la envía a cientos o miles de formularios sin protección en un intervalo breve. Cada uno de esos sitios envía un mensaje de bienvenida o de confirmación a esa dirección. En conjunto, esos mensajes ocultan el correo que la víctima realmente necesita leer.
El objetivo es la persona propietaria de esa bandeja de entrada. Mientras se llena de confirmaciones de suscripción, el atacante gasta dinero con la tarjeta de esa persona o restablece la contraseña de una de sus cuentas. La alerta de fraude del banco sigue llegando. Llega debajo de otros dos mil mensajes recibidos durante la misma hora, por lo que nadie la ve a tiempo.
Tu servidor es la herramienta con la que se ejecuta el ataque. No hay nada averiado en tu equipo. No se ha comprometido ninguna cuenta tuya. Alguien escribió una dirección en un formulario público y tu software hizo aquello para lo que fue diseñado: enviar correo a esa dirección. Esto es lo que dificulta su detección. No hay una intrusión en tus registros porque no hubo ninguna intrusión.
Cómo se ve el ataque desde su lado
Llega de una de estas dos formas.
La forma ruidosa es una ráfaga. Varios cientos de solicitudes POST llegan a un mismo formulario en pocos minutos, desde muchas direcciones IP de origen distintas y con direcciones de dominios a los que nunca ha enviado mensajes. Esta forma es fácil de detectar cuando se busca.
La forma silenciosa es la que suele pasar desapercibida. El atacante mantiene una lista de miles de formularios vulnerables, de modo que su formulario sólo tiene que recibir uno o dos envíos por hora. Jye Cusch describió un ataque exactamente de esta forma en un sitio que administra: no hubo ningún aumento del tráfico, sólo registros constantes a horas que no coincidían con las de su audiencia. Un único formulario parece inocente porque apenas está haciendo nada. El daño es la suma de todos los formularios incluidos en la lista del atacante.
Ambas formas comparten después la misma firma: no ocurre nada más. Las direcciones nunca se confirman. Nunca abren un mensaje ni hacen clic en un enlace. En una lista con confirmación de suscripción, permanecen para siempre con el estado unconfirmed, y esa acumulación es la evidencia más clara que obtendrá.
Empiece contando los envíos por minuto en el registro de acceso.
sudo awk '/POST \/subscription\/form/ {print substr($4, 2, 17)}' \
/var/log/nginx/access.log | uniq -c | sort -rn | head$4 en el formato de registro combinado predeterminado es la marca de tiempo entre corchetes, por lo que esto muestra un recuento para cada minuto, en orden descendente. Un formulario que normalmente recibe cuatro suscripciones al día y muestra sesenta en un minuto no está teniendo un buen día.
Opt-in confirmado: la defensa con mayor efecto
El opt-in confirmado, normalmente llamado doble opt-in, significa que una dirección no se convierte en suscriptor hasta que una persona hace clic en un enlace de un mensaje enviado a esa dirección. Actívelo y cada dirección enviada producirá exactamente un mensaje, una sola vez. La dirección nunca se incorpora a la lista, por lo que nunca recibe una campaña ni una secuencia de bienvenida.
En listmonk, el servidor de boletines autohospedado, es una configuración por lista: una lista usa opt-in simple o doble opt-in. La documentación explica claramente la diferencia. En una lista con doble opt-in, los suscriptores «aceptan explícitamente la suscripción al hacer clic en el correo electrónico de confirmación que reciben. Hasta entonces, no reciben mensajes de campaña». Un suscriptor permanece en unconfirmed, pasa a confirmed al hacer clic y sólo los suscriptores confirmed de una lista con opt-in reciben correo de campaña.
Sea realista sobre lo que consigue esta medida. El opt-in confirmado no reduce a cero su contribución. La limita a un mensaje por dirección. La víctima sigue recibiendo ese mensaje, y un mensaje de cada uno de mil sitios constituye todo el ataque. Lo que elimina el opt-in confirmado es todo lo que ocurre después: la lista se mantiene limpia y nunca envía un segundo mensaje a alguien que no solicitó el primero.
También importan otras dos configuraciones, y es fácil olvidarlas. Primero, limite los reenvíos de la confirmación. Si se puede enviar de nuevo la misma dirección y recibir otro correo de confirmación cada vez, el atacante no necesita mil formularios, porque el propio formulario enviará mil mensajes. Una dirección que ya esté en unconfirmed en esa lista no debería recibir nada más durante al menos un día. Segundo, elimine periódicamente las filas no confirmadas. Una dirección que no ha confirmado en treinta días no es un suscriptor pendiente. Conservarla sólo crea la posibilidad de que algo le envíe correo por accidente más adelante.
Limitar la tasa del formulario de registro en el proxy inverso
Aplique el límite delante de la aplicación, no dentro de ella. Una petición bloqueada en el proxy nunca abre una conexión a la base de datos ni inicia una conversación SMTP (simple mail transfer protocol). Un límite dentro de la aplicación se ejecuta después de que la petición ya haya consumido un proceso worker y una consulta. En muchas pilas, el mensaje incluso se pone en cola antes de que se ejecute cualquier comprobación contra abusos. El límite del proxy también permanece después de actualizar la aplicación, porque no está en el código que reemplaza.
El ejemplo siguiente usa nginx. La idea se aplica a cualquier proxy inverso que ejecute delante de su aplicación, aunque los nombres de las directivas cambian.
Añada esto al bloque http, en un archivo como /etc/nginx/conf.d/signup-limit.conf:
map $request_method $signup_key {
POST $binary_remote_addr;
default "";
}
limit_req_zone $signup_key zone=signup:10m rate=2r/m;
limit_req_status 429;
limit_req_log_level warn;map realiza una función importante. nginx no cuenta una petición cuya clave sea una cadena vacía, por lo que sólo las peticiones POST entran en la zona. Cargar varias veces la página de registro no consume nada. Sin ese map, alguien que actualizara la página dos veces agotaría su propio presupuesto antes de enviar el formulario.
$binary_remote_addr es la dirección del cliente en formato compacto. Por eso una zona de 10 megabytes contiene aproximadamente 160,000 direcciones. rate=2r/m permite un envío cada treinta segundos. limit_req_status 429 devuelve HTTP 429 Too Many Requests en lugar del 503 predeterminado de nginx. Es el código correcto y el que espera una biblioteca cliente.
A continuación, añádalo al bloque server de su sitio:
location = /subscription/form {
limit_req zone=signup burst=3 nodelay;
proxy_pass http://127.0.0.1:9000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}burst=3 nodelay permite pasar a una persona que hace doble clic en el botón y rechaza inmediatamente la cuarta petición en lugar de ponerla en cola.
sudo nginx -t && sudo systemctl reload nginxnginx -t debería mostrar configuration file /etc/nginx/nginx.conf test is successful. Ahora envíe el formulario cinco veces rápidamente y supervise el registro de errores:
sudo tail -f /var/log/nginx/error.logUna petición bloqueada escribe una línea. Esta es la cadena que debe buscar:
2026/08/13 09:14:22 [warn] 812#812: *4412 limiting requests, excess: 3.400 by zone "signup", client: 203.0.113.10, server: news.example.com, request: "POST /subscription/form HTTP/1.1", host: "news.example.com"Si no aparece ninguna línea, el límite no se está aplicando. La causa habitual es que limit_req está dentro de un bloque location al que nunca llega la petición. Compruébelo con curl -si -X POST https://news.example.com/subscription/form varias veces seguidas y confirme que obtiene un 429.
Conviene conocer dos problemas antes de depender de un límite por IP.
Detrás de una CDN u otro proxy, $binary_remote_addr es ese proxy. Todos los visitantes terminan en el mismo bloque. Por tanto, los primeros envíos de cada minuto bloquean al resto. Corríjalo con el módulo de IP real: set_real_ip_from para cada rango publicado por su CDN (Cloudflare publica los suyos en cloudflare.com/ips) y real_ip_header CF-Connecting-IP. Confirme la corrección leyendo $remote_addr en el registro de acceso y comprobando que es la dirección de un visitante, no la de su CDN.
IPv6 hace débil el límite por dirección. $binary_remote_addr contiene la /128 completa, y una asignación IPv6 residencial suele ser una /64 o mayor. Son muchas más direcciones de las que un atacante puede recorrer, cada una con su propio presupuesto disponible. Añada una segunda zona como límite máximo para el propio endpoint, usando una constante como clave, de modo que el formulario tenga una tasa total independientemente del número de direcciones de origen implicadas:
map $request_method $signup_total_key {
POST "signup";
default "";
}
limit_req_zone $signup_total_key zone=signup_total:1m rate=30r/m;Añada limit_req zone=signup_total burst=10 nodelay; al mismo location. Establezca una tasa superior a la de su hora real más concurrida y deje margen. Es un control contundente: durante un ataque también rechaza registros legítimos. Es la decisión correcta, porque la alternativa es que su servidor envíe el correo.
Por qué el límite por dirección no puede residir en el proxy
La dirección de correo llega dentro del cuerpo de la petición POST, y nginx no analiza los cuerpos de las peticiones. Todas las variables limit_req_zone que puede usar como clave proceden de la línea de petición, las cabeceras o la conexión. Por tanto, una regla como «esta dirección puede recibir como máximo una confirmación al día» debe estar en el primer componente que lee el cuerpo: la aplicación.
No intente resolverlo moviendo la dirección a la cadena de consulta para que $arg_email esté disponible. Eso escribe la dirección de cada suscriptor en texto claro en el registro de acceso y en cualquier sistema de envío de registros posterior. Cambiaría un límite de frecuencia por un problema de privacidad.
Existe una excepción real. El módulo de JavaScript de nginx, njs, puede leer el cuerpo de la petición y establecer una variable a partir de él. Esto permite crear una clave por dirección en el proxy. Es una opción válida, pero también añade código nuevo a la ruta de la petición. En la mayoría de los sitios, el límite por dirección debe estar junto a la base de datos que ya sabe si esa dirección tiene una confirmación pendiente. El proxy debe encargarse de los límites por IP y por endpoint, que son los que gestiona mejor.
No repita el texto enviado en el mensaje
Mantenga fuera del mensaje que envía cualquier cadena proporcionada por el atacante. Hay dos motivos independientes, y ambos se han utilizado en ataques reales.
Si el correo de confirmación saluda al lector con un nombre tomado del formulario, el atacante escribe su mensaje en el campo del nombre. Su servidor entrega ese texto a la víctima desde su dominio y lo firma con su clave DKIM (DomainKeys Identified Mail). Su sitio se convierte en un servicio de distribución para el abuso de otra persona, y el proveedor receptor ve su dominio en el mensaje.
El segundo motivo es más grave. Si algún campo enviado se concatena manualmente en una cabecera de correo, un carácter de nueva línea en ese campo añade cabeceras elegidas por el atacante, incluida Bcc. Las bibliotecas de correo modernas rechazan las nuevas líneas en los valores de las cabeceras. El código que canaliza texto hacia sendmail desde un script de shell a menudo no lo hace.
Un mensaje de confirmación seguro contiene el nombre del sitio y un enlace, con una sola frase explicativa. La dirección aparece únicamente donde la necesita el agente de transferencia de correo, en la cabecera To. Pruebe lo siguiente: envíe el formulario con un campo de nombre que contenga una nueva línea y un enlace evidente. Después, lea el mensaje sin procesar que recibe con less y compruebe que ninguno de los dos elementos se haya conservado.
Mientras lo hace, haga que la página de éxito muestre lo mismo para todas las direcciones. Una página que indica «ya está suscrito» para una dirección y «revise su bandeja de entrada» para otra convierte el formulario en un verificador de pertenencia para cualquiera que tenga una lista de direcciones que probar.
¿Qué comprobación contra bots debe usar?
Elija la opción de accesibilidad con el mismo cuidado que la de eficacia. Un captcha de selección de imágenes no puede resolverlo una persona ciega, y la alternativa de audio resulta difícil para personas con una audición normal. Si una comprobación hace que una persona legítima abandone el registro, esa defensa también tiene un coste. Estas son cuatro opciones, en el orden en que conviene probarlas.
Prueba de trabajo en el navegador. El navegador calcula un hash que el servidor puede verificar con poco coste, y la persona no tiene que resolver nada. listmonk ofrece esta opción en Settings y después en Security, mediante ALTCHA, que no necesita ningún servicio de terceros. En agosto de 2026, esta es la recomendación del propio listmonk frente a su opción hCaptcha obsoleta. El coste recae en quien envía más solicitudes, es decir, el atacante.
Comprobación gestionada no interactiva. Cloudflare Turnstile no muestra nada a la mayoría de los visitantes y sólo presenta un desafío cuando sus señales parecen sospechosas. Es eficaz, pero incorpora a un tercero al flujo de registro.
Campo honeypot. Es un campo de texto que una persona nunca ve y que un bot básico rellena. Asígnele un nombre que el formulario no utilice para nada más y establezca autocomplete="off", tabindex="-1" y aria-hidden="true" para impedir que un gestor de contraseñas lo rellene y que un lector de pantalla lo anuncie. Un campo llamado email2 o address se rellena automáticamente en el navegador, y entonces rechazará a personas reales.
<div style="position:absolute; left:-9999px;" aria-hidden="true">
<label for="hp_ref">Leave this field empty</label>
<input type="text" id="hp_ref" name="hp_ref" autocomplete="off" tabindex="-1">
</div>Comprobación del tiempo hasta el envío. Incluya una marca de tiempo firmada en un campo oculto cuando se muestre la página y rechace los envíos que lleguen menos de dos segundos después. Una persona no puede leer un formulario y escribir una dirección tan rápido. Firme la marca de tiempo; de lo contrario, el bot simplemente enviará una antigua.
Hay un aspecto que debe verificar independientemente de la opción elegida: el token debe consumirse una sola vez. Si un script puede resolver la comprobación una vez y reutilizar ese token contra mil direcciones, la comprobación sólo demostró que un navegador se ejecutó una vez y nada más.
¿Cómo se detecta el problema antes de que llegue el informe de abuso?
Quiere que sus propios gráficos le avisen, no el equipo de abuso del proveedor de hosting. Supervise dos aspectos.
Cuente los envíos por dirección de origen en todo el registro:
sudo awk '/POST \/subscription\/form/ {print $1}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20Después, haga que fail2ban lea las mismas líneas limiting requests que nginx ya escribe y bloquee a los infractores reincidentes. fail2ban incluye un filtro específico para esto. Cree /etc/fail2ban/jail.d/nginx-limit-req.local:
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
port = http,https
logpath = /var/log/nginx/error.log
findtime = 600
maxretry = 10
bantime = 3600sudo systemctl reload fail2ban
sudo fail2ban-client status nginx-limit-reqLa salida de estado muestra el filtro de la jail y los recuentos actuales de fallos y bloqueos. Currently banned: 0 en un día tranquilo es correcto. Si la jail no aparece, fail2ban nunca cargó el archivo, y sudo fail2ban-client -d | grep nginx-limit-req muestra la configuración que realmente analizó. El filtro incluido coincide con todas las zonas limit_req. Limítelo a su zona de registro estableciendo ngx_limit_req_zones = signup en una sección [Definition] de /etc/fail2ban/filter.d/nginx-limit-req.local. La estructura de los archivos de jail y los comandos de bloqueo se explican con más detalle en la guía de fail2ban para Ubuntu 24.04.
La segunda señal es una proporción y no requiere software nuevo: envíos divididos por confirmaciones. En una lista saludable, la mayoría de las personas que envían una dirección hacen clic en el enlace, normalmente bastante más de la mitad. Si esa proporción se desploma mientras aumentan los envíos, están utilizando su sistema. Compare el número de suscriptores unconfirmed creados durante la última hora con el número de confirmed, según el calendario que ya utilice para ejecutar los informes.
Lo que le cuesta: reputación del remitente y listas de bloqueo
Esta es la parte que convierte una molestia en una factura.
Las listas de direcciones que se usan para los bombardeos se recopilan, y las listas recopiladas contienen spamtraps: direcciones que nunca se registraron en ningún servicio y que sólo se publican para detectar a los remitentes que envían mensajes sin permiso. El mensaje de confirmación llega a una de ellas. Algunos operadores de listas de bloqueo no necesitan nada más.
Los destinatarios que nunca solicitaron su mensaje no hacen clic en cancelar la suscripción. Hacen clic en «reportar spam». Las reglas de envío masivo de Google, vigentes desde February 2024, indican a los remitentes de 5,000 o más mensajes diarios a Gmail que mantengan la tasa de spam denunciado en Postmaster Tools por debajo de 0.3%. Un remitente más pequeño no se evalúa con ese número, pero la misma señal de quejas alimenta las decisiones de filtrado que envían sus mensajes a la carpeta de spam. Las direcciones falsas de la ejecución también generan rebotes permanentes, y una tasa creciente de rebotes permanentes es una señal de reputación independiente en todos los proveedores grandes.
Si ejecuta su propio servidor de correo en un VPS con mailcow, la inclusión afecta a su dirección IP y a su dominio. Para solicitar la eliminación con un operador como Spamhaus debe rellenar un formulario y esperar. Mientras espera, sus facturas y los restablecimientos de contraseña tampoco se entregarán. Si, en cambio, envía los mensajes a través de un proveedor compartido, espere que suspenda primero su cuenta y lea después su explicación, porque su tráfico supone un riesgo para todos los demás remitentes de esa IP.
Frente a eso, el trabajo es pequeño. Active hoy el opt-in confirmado, porque es una sola opción por lista. Añada después el límite de tasa en el proxy, porque consiste en un archivo y una recarga. La comprobación del bot y las alertas pueden quedar para esta semana.
FAQ
¿El doble opt-in detiene el bombardeo de suscripciones?
Evita que la lista se contamine y limita tu contribución a un mensaje por cada dirección enviada, que es la mayor mejora individual disponible. No evita que se llene la bandeja de entrada de la víctima, porque el ataque consiste en la suma de un mensaje procedente de cada uno de mil sitios. Combínalo con un límite de tasa por IP en el proxy y un límite de reenvíos de confirmación, para que enviar la misma dirección dos veces no produzca un segundo mensaje.
¿Cómo distingo una campaña de bombardeo de un buen día de suscripciones reales?
Observa qué ocurre después del envío. Las suscripciones reales confirman la dirección y normalmente lo hacen en unas horas. Una campaña de bombardeo deja un conjunto de direcciones que nunca confirman, nunca abren el mensaje y nunca hacen clic. Los envíos también se agrupan de forma extraña: muchas direcciones de origen que nunca habías visto, dominios de destinatarios a los que normalmente no envías mensajes y horas de llegada distribuidas de manera uniforme durante todo el día, en lugar de seguir las horas de actividad habituales de tu audiencia.
¿Debo eliminar las direcciones enviadas?
Sí. Elimina los registros no confirmados con más de unos treinta días y hazlo mediante una tarea programada, no manualmente. Nunca envíes nada más a esas direcciones, ni siquiera una disculpa o un mensaje de «¿has sido tú?», porque sería un segundo mensaje no solicitado para alguien que ya ha recibido muchos. Si alguna de esas direcciones era una spamtrap, un seguimiento es precisamente la confirmación que espera el operador de la blocklist.
¿El límite de tasa rechazará a suscriptores reales?
Un límite por IP de un envío cada treinta segundos, con una ráfaga de tres, es imperceptible para una persona que rellena un formulario una vez. Se hace visible cuando muchas personas reales comparten una dirección, por ejemplo, una oficina detrás de una única puerta de enlace NAT (traducción de direcciones de red), o cuando el proxy ve la dirección de tu CDN en lugar de la del visitante. Lee $remote_addr en el registro de acceso antes de ajustar el límite y mantén el tope del endpoint por encima de la hora real de mayor actividad.
Mi IP de envío está en una blocklist después de una campaña. ¿Qué hago primero?
Deja de enviar desde ella antes de solicitar nada. Pausa la cola de campañas, corrige el formulario y elimina las direcciones no confirmadas, porque una eliminación de la blocklist seguida de más tráfico igual hará que vuelvas a aparecer en ella más rápido que la primera vez. Después, averigua en qué lista estás, ya que la mayoría de los operadores tienen una página de consulta basada en tu dirección IP, y sigue su proceso de eliminación. Espera que el proceso tarde varios días y utiliza ese tiempo para confirmar que tu registro SPF (marco de políticas del remitente) y la firma DKIM siguen superando las comprobaciones.