SSD Nodes Learn 🎉 VPS dès $5.50/mois
Guides Matt ConnorPar Matt Connor

Bloquer le bombardement d’abonnements sur vos formulaires

Un attaquant peut inscrire une adresse à des centaines de services. Le confirmed opt-in et les rate limits empêchent votre serveur d’envoyer ces messages en masse.

Qu’est-ce que le bombardement d’abonnements ?

Le bombardement d’abonnements est une attaque qui utilise votre formulaire d’inscription pour saturer la boîte de réception d’une autre personne. L’attaquant prend l’adresse e-mail d’une victime et la saisit dans des centaines ou des milliers de formulaires mal protégés en peu de temps. Chacun de ces sites envoie un message de bienvenue ou de confirmation à cette adresse. Ensemble, ces messages masquent les e-mails que la victime doit réellement lire.

La cible est la personne qui possède cette boîte de réception. Pendant qu’elle se remplit de confirmations d’abonnement, l’attaquant dépense de l’argent avec la carte de cette personne ou réinitialise le mot de passe de l’un de ses comptes. L’alerte de fraude envoyée par la banque arrive tout de même. Elle arrive sous deux mille autres messages reçus au cours de la même heure, et personne ne la voit à temps.

Votre serveur est l’outil utilisé pour mener l’attaque. Rien n’est défaillant sur votre serveur. Aucun de vos comptes n’a été compromis. Quelqu’un a saisi une adresse dans un formulaire public, et votre logiciel a fait ce pour quoi il a été conçu : envoyer un e-mail à cette adresse. C’est ce qui rend l’attaque difficile à détecter. Vos journaux ne contiennent aucune intrusion, puisqu’il n’y en a pas eu.

À quoi ressemble l’attaque de votre côté

Elle se présente sous l’une de deux formes.

La forme bruyante est une rafale. Plusieurs centaines de requêtes POST atteignent un même formulaire en quelques minutes. Elles proviennent de nombreuses adresses IP source différentes et contiennent des adresses appartenant à des domaines auxquels vous n’avez jamais envoyé de message. Cette forme est facile à repérer dès que vous l’examinez.

La forme silencieuse est celle qui passe inaperçue. L’attaquant dispose d’une liste de milliers de formulaires vulnérables. Votre formulaire ne doit donc traiter qu’une ou deux soumissions par heure. Jye Cusch a décrit une attaque exactement de cette forme sur un site qu’il gère : aucun pic de trafic, seulement des inscriptions régulières arrivant à des heures qui ne correspondaient pas à celles de son audience. Un seul formulaire semble normal, car il ne fait presque rien. Les dommages correspondent à la somme de ce qui se produit sur tous les formulaires de la liste de l’attaquant.

Les deux formes présentent ensuite la même signature : rien ne se passe. Les adresses ne sont jamais confirmées. Les destinataires n’ouvrent jamais les messages et ne cliquent jamais sur les liens. Sur une liste avec double opt-in, elles restent indéfiniment à l’état unconfirmed. Cette accumulation constitue l’indice le plus clair dont vous disposerez.

Commencez par compter les soumissions par minute dans votre access log.

sudo awk '/POST \/subscription\/form/ {print substr($4, 2, 17)}' \
  /var/log/nginx/access.log | uniq -c | sort -rn | head

$4 dans le format de journal combiné par défaut correspond à l’horodatage entre crochets. Cette commande affiche donc le nombre de soumissions pour chaque minute, par ordre décroissant. Un formulaire qui reçoit normalement quatre inscriptions par jour et qui en affiche soixante en une minute ne connaît pas une journée normale.

Confirmation d’inscription : la défense la plus efficace

La confirmation d’inscription, généralement appelée double opt-in, signifie qu’une adresse n’est pas abonnée tant qu’une personne n’a pas cliqué sur un lien dans un message envoyé à cette adresse. Activez cette option : une adresse soumise produit exactement un message, une seule fois. L’adresse ne rejoint jamais la liste. Elle ne reçoit donc ni campagne ni séquence de bienvenue.

Dans listmonk, le serveur de newsletter auto-hébergé, ce réglage s’applique à chaque liste : une liste utilise le simple opt-in ou le double opt-in. La documentation décrit clairement la différence. Sur une liste en double opt-in, les abonnés « acceptent explicitement l’inscription en cliquant sur l’e-mail de confirmation qu’ils reçoivent. Jusque-là, ils ne reçoivent pas les messages de campagne. » Un abonné reste à l’état unconfirmed, passe à l’état confirmed après le clic, et seuls les abonnés confirmed d’une liste en opt-in reçoivent les e-mails de campagne.

Soyez précis sur le bénéfice obtenu. La confirmation d’inscription ne réduit pas votre contribution à zéro. Elle la limite à un message par adresse. La victime reçoit toujours ce message, et un message provenant de chacun de mille sites suffit à constituer toute l’attaque. La confirmation d’inscription supprime tout ce qui suit : votre liste reste propre et vous n’envoyez jamais de second message à une personne qui n’a pas demandé le premier.

Deux autres réglages sont importants et faciles à oublier. Premièrement, limitez les renvois de la confirmation. Si la même adresse peut être soumise à nouveau et recevoir un nouvel e-mail de confirmation à chaque fois, l’attaquant n’a pas besoin de mille formulaires : votre seul formulaire enverra mille messages. Une adresse déjà à l’état unconfirmed sur cette liste ne devrait plus rien recevoir pendant au moins un jour. Deuxièmement, supprimez les lignes non confirmées selon une planification. Une adresse qui n’a pas confirmé dans les trente jours n’est pas un abonné en attente. La conserver crée seulement un risque qu’un message lui soit envoyé accidentellement plus tard.

Limiter le formulaire d’inscription au niveau du reverse proxy

Placez la limite devant l’application plutôt qu’à l’intérieur. Une requête bloquée par le proxy n’ouvre jamais de connexion à la base de données et n’établit jamais de conversation SMTP (simple mail transfer protocol). Une limite appliquée dans l’application intervient après que la requête a déjà consommé un processus worker et exécuté une requête SQL. Dans de nombreuses stacks, le message est même mis en file d’attente avant l’exécution du contrôle anti-abus. La limite du proxy reste également en place après une mise à niveau de l’application, car elle ne se trouve pas dans le code que vous remplacez.

L’exemple ci-dessous utilise nginx. Le principe s’applique à n’importe quel reverse proxy placé devant votre application, même si les noms des directives diffèrent.

Ajoutez ceci dans le bloc http, dans un fichier tel que /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;

La directive map fait le travail réel. nginx ne compte pas les requêtes dont la clé est une chaîne vide. Seules les requêtes POST entrent donc dans la zone. Le chargement répété de la page d’inscription ne consomme rien. Sans cette map, une personne qui recharge la page deux fois consommerait sa propre limite avant même d’envoyer le formulaire.

$binary_remote_addr correspond à l’adresse du client sous forme compacte. C’est pourquoi une zone de 10 megabytes peut en contenir environ 160,000. rate=2r/m autorise une soumission toutes les trente secondes. limit_req_status 429 renvoie HTTP 429 Too Many Requests au lieu du code 503 par défaut de nginx. C’est le code approprié et celui qu’attend une bibliothèque cliente.

Ajoutez ensuite ceci dans le bloc server de votre site :

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 laisse passer une personne qui double-clique sur le bouton et rejette immédiatement la quatrième requête au lieu de la mettre en file d’attente.

sudo nginx -t && sudo systemctl reload nginx

nginx -t doit afficher configuration file /etc/nginx/nginx.conf test is successful. Envoyez maintenant le formulaire cinq fois rapidement et surveillez l’error log :

sudo tail -f /var/log/nginx/error.log

Une requête bloquée écrit une ligne. Voici la chaîne à rechercher :

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"

L’absence totale de ligne signifie que la limite ne s’applique pas. La cause habituelle est que limit_req se trouve dans un bloc location que la requête n’atteint jamais. Vérifiez donc avec curl -si -X POST https://news.example.com/subscription/form plusieurs fois de suite et confirmez que vous obtenez un 429.

Deux pièges sont à connaître avant de vous appuyer sur une limite par adresse IP.

Derrière un CDN ou un autre proxy, $binary_remote_addr correspond à ce proxy. Tous les visiteurs se retrouvent dans le même bucket. Les premières soumissions de chaque minute bloquent donc tous les autres visiteurs. Corrigez cela avec le module real IP : set_real_ip_from pour chacune des plages publiées par votre CDN (Cloudflare les liste sur cloudflare.com/ips), puis real_ip_header CF-Connecting-IP. Confirmez la correction en lisant $remote_addr dans votre access log et en vérifiant qu’il s’agit de l’adresse d’un visiteur, et non de celle de votre CDN.

IPv6 rend une limite par adresse peu efficace. $binary_remote_addr conserve l’adresse /128 complète, alors qu’une allocation IPv6 résidentielle est généralement un /64 ou plus large. Cela représente bien plus d’adresses qu’un attaquant ne peut en utiliser, chacune disposant de sa propre limite. Ajoutez une seconde zone comme plafond pour l’endpoint lui-même, avec une clé constante. Le formulaire aura ainsi une limite globale, quel que soit le nombre d’adresses source utilisées :

map $request_method $signup_total_key {
    POST    "signup";
    default "";
}

limit_req_zone $signup_total_key zone=signup_total:1m rate=30r/m;

Ajoutez limit_req zone=signup_total burst=10 nodelay; au même location. Définissez un rate supérieur à celui de votre heure réelle la plus chargée, avec une marge suffisante. Il s’agit d’un contrôle volontairement large : pendant une attaque, il bloquera également de véritables inscriptions. C’est le compromis approprié, car l’autre possibilité est que votre serveur envoie les e-mails.

La limite par adresse ne peut pas être appliquée au niveau du proxy

L’adresse e-mail se trouve dans le corps de la requête POST, et nginx n’analyse pas les corps de requête. Toutes les variables sur lesquelles limit_req_zone peut s’appuyer proviennent de la ligne de requête, des en-têtes ou de la connexion. Une règle comme « cette adresse peut recevoir au maximum une confirmation par jour » doit donc être appliquée par le premier composant qui lit le corps de la requête : votre application.

Ne contournez pas ce problème en déplaçant l’adresse dans la query string pour rendre $arg_email disponible. L’adresse de chaque abonné serait alors inscrite en clair dans votre access log, ainsi que dans tous les systèmes de collecte de logs situés en aval. Vous remplaceriez une limitation de débit par un problème de confidentialité.

Il existe une véritable exception. Le module JavaScript de nginx, njs, peut lire le corps de la requête et définir une variable à partir de son contenu. Vous pouvez ainsi créer une clé par adresse au niveau du proxy. C’est une option réelle, mais elle ajoute aussi du code dans le chemin de traitement de vos requêtes. Pour la plupart des sites, la limite par adresse doit être gérée à proximité de la base de données qui sait déjà si cette adresse a une confirmation en attente. Le proxy peut alors gérer les limites par IP et par endpoint, pour lesquelles il est adapté.

Ne répétez pas le texte soumis dans le message

Ne laissez aucune chaîne fournie par l’attaquant apparaître dans le message envoyé. Deux raisons distinctes le justifient, et ces deux techniques ont déjà été utilisées dans des attaques.

Si votre e-mail de confirmation s’adresse au lecteur par un nom extrait du formulaire, l’attaquant écrit son message dans le champ du nom. Votre serveur transmet ensuite ce texte à la victime depuis votre domaine, avec votre clé DKIM (DomainKeys Identified Mail). Votre site devient un service de diffusion pour les abus d’un tiers, et le fournisseur destinataire voit votre domaine dans le message.

La seconde raison est plus grave. Si un champ soumis est concaténé manuellement dans un en-tête d’e-mail, un caractère de nouvelle ligne dans ce champ ajoute des en-têtes choisis par l’attaquant, notamment Bcc. Les bibliothèques de messagerie modernes refusent les retours à la ligne dans les valeurs d’en-tête. En revanche, un script shell qui transmet du texte à sendmail ne les bloque souvent pas.

Un message de confirmation sûr contient le nom de votre site et un lien, avec une phrase d’explication. L’adresse apparaît uniquement à l’endroit où l’agent de transfert de courrier en a besoin, dans l’en-tête To. Testez ce comportement : envoyez le formulaire avec un champ de nom contenant une nouvelle ligne et un lien évident, puis lisez le message brut reçu avec less et vérifiez qu’aucun des deux n’y figure.

Profitez-en pour faire afficher le même message sur la page de réussite, quelle que soit l’adresse. Une page qui indique « vous êtes déjà inscrit » pour une adresse et « consultez votre boîte de réception » pour une autre transforme votre formulaire en outil de vérification d’adhésion pour toute personne disposant d’une liste d’adresses à tester.

Quel contrôle anti-bot utiliser ?

Choisissez une solution accessible avec autant de soin qu’une solution efficace. Un captcha de sélection d’images est inutilisable par une personne aveugle, et le recours audio est difficile pour les personnes ayant une audition ordinaire. Un contrôle qui fait renoncer une personne légitime à son inscription est aussi un coût pour votre défense. Voici quatre options, dans l’ordre à essayer.

Une preuve de travail dans le navigateur. Le navigateur calcule un hash que le serveur peut vérifier à peu de frais, et la personne n’a rien à résoudre. listmonk propose cette option dans Settings, puis Security, avec ALTCHA, qui ne nécessite aucun service tiers. En août 2026, c’est la recommandation de listmonk, à la place de l’option hCaptcha obsolète. Le coût revient à celui qui soumet le plus de requêtes, c’est-à-dire l’attaquant.

Un contrôle géré sans interaction. Cloudflare Turnstile n’affiche généralement rien à la plupart des visiteurs et ne les soumet à un contrôle que si ses signaux semblent suspects. Il est efficace, mais il fait intervenir un tiers dans votre parcours d’inscription.

Un champ honeypot. Il s’agit d’un champ texte qu’une personne ne voit jamais, mais qu’un bot basique remplit. Donnez-lui un nom que votre formulaire n’utilise pas autrement, puis définissez autocomplete="off", tabindex="-1" et aria-hidden="true" afin qu’un gestionnaire de mots de passe ne le remplisse pas et qu’un lecteur d’écran ne l’annonce pas. Un champ nommé email2 ou address est rempli automatiquement par le navigateur ; vous rejetteriez alors de vraies personnes.

<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>

Un contrôle du délai avant l’envoi. Placez un timestamp signé dans un champ masqué lorsque la page est affichée, puis rejetez toute soumission reçue moins de deux secondes plus tard. Une personne ne peut pas lire un formulaire et saisir une adresse aussi rapidement. Signez le timestamp, sinon le bot en enverra simplement un ancien.

Vérifiez un point, quelle que soit l’option choisie : le token doit être consommé une seule fois. Si un script peut réussir le contrôle une fois, puis rejouer ce token avec mille adresses, le contrôle aura seulement prouvé qu’un navigateur a été exécuté une fois, et rien de plus.

Comment le détecter avant de recevoir le signalement d’abus ?

Vous voulez que vos propres graphiques vous alertent, et non le service chargé des abus de votre hébergeur. Surveillez deux éléments.

Comptez les soumissions par adresse source dans l’ensemble du journal :

sudo awk '/POST \/subscription\/form/ {print $1}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -rn | head -20

Demandez ensuite à fail2ban de lire les mêmes lignes limiting requests déjà écrites par nginx, puis de bannir les récidivistes. fail2ban fournit un filtre prévu exactement pour ce cas. Créez /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  = 3600
sudo systemctl reload fail2ban
sudo fail2ban-client status nginx-limit-req

La sortie d’état indique le filtre de la jail, ainsi que le nombre actuel d’échecs et de bannissements. Currently banned: 0 un jour calme est normal. Si la jail n’apparaît pas du tout, fail2ban n’a jamais chargé le fichier. Dans ce cas, sudo fail2ban-client -d | grep nginx-limit-req affiche la configuration effectivement analysée. Le filtre fourni correspond à chaque zone limit_req. Limitez-le à votre zone d’inscription en définissant ngx_limit_req_zones = signup dans une section [Definition] de /etc/fail2ban/filter.d/nginx-limit-req.local. La structure des fichiers de jail et les commandes de bannissement sont expliquées plus en détail dans le guide fail2ban pour Ubuntu 24.04.

Le second indicateur est un ratio et ne nécessite aucun nouveau logiciel : le nombre de soumissions divisé par le nombre de confirmations. Sur une liste saine, la plupart des personnes qui soumettent une adresse cliquent sur le lien, généralement bien plus de la moitié. Lorsque ce ratio s’effondre alors que le nombre de soumissions augmente, votre service est utilisé à votre insu. Comparez le nombre d’abonnés unconfirmed créés au cours de la dernière heure avec le nombre d’abonnés confirmed, selon la périodicité à laquelle vous générez déjà vos rapports.

Ce que cela vous coûte : réputation de l’expéditeur et blocklists

C’est ce qui transforme une nuisance en facture.

Les listes d’adresses utilisées pour ces campagnes sont récupérées automatiquement. Elles contiennent des spamtraps : des adresses qui ne se sont jamais inscrites nulle part et qui sont publiées uniquement pour détecter les expéditeurs qui envoient des messages sans autorisation. Votre message de confirmation atteint l’une d’elles. Certains opérateurs de blocklists n’ont besoin de rien de plus.

Les destinataires qui n’ont jamais demandé votre message ne cliquent pas sur le lien de désinscription. Ils cliquent sur « signaler comme spam ». Les règles de Google applicables aux expéditeurs en masse, en vigueur depuis février 2024, demandent aux expéditeurs de 5,000 messages ou plus par jour vers Gmail de maintenir le taux de spam signalé dans Postmaster Tools sous 0.3 %. Un expéditeur plus petit n’est pas évalué selon ce seuil, mais le même signal de plainte alimente les décisions de filtrage qui placent vos messages dans le dossier spam. Les fausses adresses utilisées pendant l’attaque génèrent aussi des hard bounces, et l’augmentation du taux de hard bounces constitue un signal de réputation distinct chez tous les grands fournisseurs.

Si vous utilisez votre propre serveur de messagerie sur un VPS avec mailcow, l’inscription concerne votre adresse IP et votre domaine. Une demande de retrait auprès d’un opérateur comme Spamhaus implique de remplir un formulaire, puis d’attendre. Pendant ce délai, vos factures et vos réinitialisations de mot de passe ne sont pas livrées non plus. Si vous envoyez vos messages via un fournisseur mutualisé, attendez-vous à ce qu’il suspende d’abord votre compte et examine ensuite vos explications, car votre trafic représente un risque pour tous les autres expéditeurs qui utilisent cette adresse IP.

En comparaison, le travail nécessaire est limité. Activez dès aujourd’hui le confirmed opt-in, car cela consiste en un seul réglage par liste. Ajoutez ensuite la limitation de débit sur le proxy, car elle nécessite un seul fichier et un reload. Vous pourrez mettre en place le contrôle des bots et les alertes cette semaine.

FAQ

Le double opt-in empêche-t-il le subscription bombing ?

Il empêche la pollution de votre liste et limite votre contribution à un message par adresse soumise, ce qui constitue la principale amélioration à votre portée. Il n’empêche pas la boîte de réception de la victime de se remplir, car l’attaque correspond à la somme d’un message provenant de chacun de mille sites. Associez-le à une limite de débit par IP sur votre proxy et à un plafond de renvois de confirmation, afin que la soumission deux fois de la même adresse ne produise pas un second message.

Comment distinguer une campagne de bombing d’une journée normale d’inscriptions réelles ?

Observez ce qui se passe après la soumission. Les inscriptions réelles sont confirmées, généralement dans les heures qui suivent. Une campagne de bombing laisse un ensemble d’adresses qui ne sont jamais confirmées, n’ouvrent jamais les messages et ne cliquent jamais. Les soumissions présentent aussi des regroupements inhabituels : de nombreuses adresses sources que vous n’avez jamais vues, des domaines destinataires auxquels vous n’envoyez généralement rien et des heures d’arrivée réparties uniformément sur toute la journée au lieu de suivre les heures d’éveil de votre audience.

Dois-je supprimer les adresses qui ont été soumises ?

Oui. Supprimez les enregistrements non confirmés datant de plus d’environ trente jours et faites-le selon une planification plutôt que manuellement. N’envoyez jamais rien d’autre à ces adresses, y compris un message d’excuse ou un message demandant « était-ce vous ? », car il s’agirait d’un second message non sollicité envoyé à quelqu’un qui en a déjà reçu un grand nombre. Si certaines de ces adresses sont des spamtraps, un suivi constitue la confirmation qu’attend l’opérateur de la blocklist.

La limitation de débit rejettera-t-elle les vrais abonnés ?

Une limite par IP d’une soumission toutes les trente secondes, avec un burst de trois, est invisible pour une personne qui remplit un formulaire une seule fois. Elle devient visible lorsque de nombreuses personnes partagent une même adresse, par exemple dans un bureau situé derrière une passerelle NAT (network address translation) unique, ou lorsque votre proxy voit l’adresse de votre CDN au lieu de celle du visiteur. Lisez $remote_addr dans votre access log avant de resserrer les limites et maintenez le plafond du endpoint au-dessus de votre heure réelle la plus chargée.

Mon IP d’envoi figure sur une blocklist après une campagne. Que dois-je faire en premier ?

Arrêtez d’envoyer depuis cette IP avant d’entreprendre toute autre démarche. Mettez la file de campagne en pause, corrigez le formulaire et supprimez les adresses non confirmées, car une sortie de blocklist suivie d’un trafic identique vous y fera réinscrire plus rapidement que la première fois. Identifiez ensuite la liste concernée, car la plupart des opérateurs proposent une page de recherche basée sur votre adresse IP, puis suivez leur procédure de retrait. Prévoyez un délai de plusieurs jours et profitez-en pour vérifier que votre enregistrement SPF (sender policy framework) et votre signature DKIM passent toujours les contrôles.

#email#double-opt-in#rate-limiting#abuse#deliverability