Come bloccare il bombardamento di iscrizioni
Un attaccante invia l'indirizzo di una vittima a centinaia di moduli. Il double opt-in e i limiti di frequenza impediscono al server di inviare email indesiderate.
Che cos'è il bombardamento di iscrizioni?
Il bombardamento di iscrizioni è un attacco che usa il modulo di registrazione per riempire la casella di posta di un'altra persona. L'attaccante prende l'indirizzo email di una vittima e lo invia a centinaia o migliaia di moduli non protetti in un breve intervallo. Ciascun sito invia a quell'indirizzo un messaggio di benvenuto o di conferma. Nel complesso, questi messaggi nascondono le email che la vittima deve effettivamente leggere.
L'obiettivo è la persona proprietaria della casella di posta. Mentre la casella si riempie di conferme di iscrizione, l'attaccante usa la carta di quella persona per effettuare acquisti o reimposta la password di uno dei suoi account. L'avviso di frode della banca arriva comunque. Arriva sotto altri duemila messaggi ricevuti nella stessa ora, quindi nessuno lo vede in tempo.
Il tuo server è lo strumento con cui viene eseguito l'attacco. Sul tuo server non c'è nulla di guasto. Nessun tuo account è stato compromesso. Qualcuno ha inserito un indirizzo in un modulo pubblico e il tuo software ha fatto ciò per cui è stato progettato: ha inviato un'email a quell'indirizzo. È questo che rende l'attacco difficile da rilevare. Nei log non c'è alcuna intrusione, perché non si è verificata alcuna intrusione.
Come appare l’attacco dal tuo lato
L’attacco si presenta in una di due forme.
La forma rumorosa è un picco improvviso. In pochi minuti, diverse centinaia di richieste POST raggiungono lo stesso modulo, provenendo da molti indirizzi IP di origine diversi e contenendo indirizzi appartenenti a domini a cui non hai mai inviato messaggi. Questa forma è facile da individuare quando la cerchi.
La forma silenziosa è quella che spesso non viene rilevata. L’aggressore dispone di un elenco di migliaia di moduli vulnerabili, quindi il tuo modulo deve contribuire soltanto con uno o due invii all’ora. Jye Cusch ha descritto un attacco esattamente di questo tipo su un sito che gestisce: nessun picco di traffico, ma iscrizioni regolari ricevute in orari che non corrispondevano al suo pubblico. Un singolo modulo sembra innocuo perché, preso da solo, esegue un numero minimo di operazioni. Il danno è la somma degli invii ricevuti da tutti i moduli presenti nell’elenco dell’aggressore.
Dopo l’attacco, entrambe le forme hanno lo stesso segnale: non accade nulla. Gli indirizzi non vengono mai confermati. I destinatari non aprono mai un messaggio e non fanno mai clic su un link. In un elenco con double opt-in restano per sempre con stato unconfirmed, e questo insieme di indirizzi è l’indicatore più chiaro che puoi ottenere.
Inizia contando gli invii al minuto nel tuo access log.
sudo awk '/POST \/subscription\/form/ {print substr($4, 2, 17)}' \
/var/log/nginx/access.log | uniq -c | sort -rn | headNel formato di log combinato predefinito, $4 indica il timestamp tra parentesi quadre. Il comando stampa quindi il conteggio per ogni minuto, in ordine decrescente. Se un modulo che normalmente riceve quattro iscrizioni al giorno ne mostra sessanta in un minuto, non è una giornata positiva.
Opt-in confermato: la difesa con l'effetto maggiore
L'opt-in confermato, generalmente chiamato double opt-in, significa che un indirizzo non diventa iscritto finché una persona non fa clic su un link contenuto in un messaggio inviato a quell'indirizzo. Attivandolo, ogni indirizzo inviato tramite un modulo produce esattamente un messaggio, una sola volta. L'indirizzo non entra mai nella lista, quindi non riceve né una campagna né una sequenza di benvenuto.
In listmonk, il server self-hosted per newsletter, questa è un'impostazione per singola lista: una lista usa l'opt-in singolo oppure il double opt-in. La documentazione chiarisce senza ambiguità la differenza. In una lista con double opt-in, gli iscritti "accettano esplicitamente l'iscrizione facendo clic sull'e-mail di conferma che ricevono. Fino a quel momento non ricevono messaggi delle campagne." Un iscritto si trova in unconfirmed, passa a confirmed dopo il clic e solo gli iscritti confirmed di una lista con opt-in ricevono le e-mail delle campagne.
È importante essere chiari sul risultato. L'opt-in confermato non porta a zero il tuo contributo. Lo limita a un messaggio per indirizzo. La vittima riceve comunque quel messaggio e un messaggio proveniente da ciascuno di mille siti è sufficiente per l'intero attacco. L'opt-in confermato elimina tutto ciò che viene dopo: la tua lista resta pulita e non invii mai un secondo messaggio a chi non aveva mai richiesto il primo.
Contano anche altre due impostazioni, entrambe facili da dimenticare. Prima di tutto, limita i nuovi invii della conferma. Se lo stesso indirizzo può essere inviato di nuovo e ricevere ogni volta un'altra e-mail di conferma, all'attaccante non servono mille moduli, perché sarà il tuo stesso modulo a inviare mille messaggi. Un indirizzo che si trova già in unconfirmed su quella lista non dovrebbe ricevere altro per almeno un giorno. In secondo luogo, elimina periodicamente le righe non confermate. Un indirizzo che non ha confermato entro trenta giorni non è un iscritto in attesa. Conservarlo crea soltanto la possibilità che in seguito gli venga inviata per errore qualche comunicazione.
Limitare la frequenza del modulo di registrazione nel reverse proxy
Applica il limite davanti all'applicazione, non al suo interno. Una richiesta bloccata dal proxy non apre mai una connessione al database e non avvia mai una conversazione SMTP (simple mail transfer protocol). Un limite interno all'applicazione viene applicato dopo che la richiesta ha già consumato un processo worker e una query; in molti stack, inoltre, il messaggio viene accodato prima che venga eseguito qualsiasi controllo contro gli abusi. Il limite del proxy resta attivo anche dopo un aggiornamento dell'applicazione, perché non risiede nel codice che sostituisci.
L'esempio seguente usa nginx. Il principio si applica a qualunque reverse proxy eseguito davanti all'applicazione, anche se i nomi delle direttive differiscono.
Inserisci quanto segue nel blocco http, in un file come /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 svolge una funzione effettiva. nginx non conta una richiesta la cui chiave è una stringa vuota, quindi solo le richieste POST entrano nella zone. Il caricamento ripetuto della pagina di registrazione non consuma nulla. Senza questa map, chi aggiorna la pagina due volte consumerebbe il proprio budget prima ancora di inviare il modulo.
$binary_remote_addr è l'indirizzo del client in formato compatto; per questo una zone da 10 megabyte ne contiene circa 160,000. rate=2r/m consente un invio ogni trenta secondi. limit_req_status 429 restituisce HTTP 429 Too Many Requests invece del codice predefinito 503 di nginx. È il codice corretto e quello previsto da una libreria client.
Poi, nel blocco server relativo al sito:
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 consente il passaggio a chi fa doppio clic sul pulsante e rifiuta subito la quarta richiesta invece di accodarla.
sudo nginx -t && sudo systemctl reload nginxnginx -t dovrebbe stampare configuration file /etc/nginx/nginx.conf test is successful. Ora invia rapidamente il modulo cinque volte e monitora il log degli errori:
sudo tail -f /var/log/nginx/error.logUna richiesta bloccata scrive una riga. Questa è la stringa da cercare:
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'assenza totale di righe indica che il limite non viene applicato. La causa abituale è che limit_req si trova in un blocco location che la richiesta non raggiunge. Esegui quindi curl -si -X POST https://news.example.com/subscription/form alcune volte consecutive e verifica di ottenere un 429.
Prima di affidarti a un limite per indirizzo IP, considera due problemi.
Dietro un CDN o un altro proxy, $binary_remote_addr è l'indirizzo del proxy. Tutti i visitatori finiscono nello stesso bucket, quindi le prime richieste di ogni minuto bloccano tutti gli altri. Risolvi il problema con il modulo per l'IP reale: set_real_ip_from per ciascuno degli intervalli pubblicati dal tuo CDN (Cloudflare li elenca in cloudflare.com/ips) e real_ip_header CF-Connecting-IP. Verifica la correzione leggendo $remote_addr nel log degli accessi e controllando che sia l'indirizzo di un visitatore, non quello del CDN.
IPv6 rende debole un limite per indirizzo. $binary_remote_addr conserva l'intero prefisso /128, mentre un'assegnazione IPv6 residenziale è in genere /64 o più ampia. Si tratta di molti più indirizzi di quanti un attaccante possa utilizzare, ciascuno con un budget indipendente. Aggiungi una seconda zone come limite massimo sull'endpoint stesso, usando una costante come chiave, in modo che il modulo abbia una frequenza complessiva limitata indipendentemente dal numero di indirizzi sorgente utilizzati:
map $request_method $signup_total_key {
POST "signup";
default "";
}
limit_req_zone $signup_total_key zone=signup_total:1m rate=30r/m;Aggiungi limit_req zone=signup_total burst=10 nodelay; alla stessa location. Imposta una frequenza superiore a quella dell'ora reale più intensa, lasciando un margine adeguato. È un controllo indiscriminato: durante un attacco blocca anche registrazioni legittime. È il compromesso corretto, perché l'alternativa è che sia il server a inviare la posta.
Perché il limite per indirizzo non può risiedere nel proxy
L'indirizzo email arriva nel corpo della richiesta POST e nginx non analizza i corpi delle richieste. Ogni variabile su cui limit_req_zone può basarsi proviene dalla riga della richiesta, dagli header o dalla connessione. Una regola come «questo indirizzo può ricevere al massimo una conferma al giorno» deve quindi essere applicata dal primo componente che legge il corpo della richiesta, cioè dall'applicazione.
Non aggirare il problema spostando l'indirizzo nella query string, in modo da rendere disponibile $arg_email. In questo modo l'indirizzo di ogni iscritto finirebbe in chiaro nel log degli accessi e in qualsiasi sistema di raccolta log a valle. Sostituiresti un limite di frequenza con un problema di privacy.
Esiste un'eccezione concreta. Il modulo JavaScript di nginx, njs, può leggere il corpo della richiesta e impostare una variabile in base al suo contenuto. Questo permette di creare una chiave per indirizzo nel proxy. È un'opzione reale, ma introduce anche nuovo codice nel percorso della richiesta. Per la maggior parte dei siti, il limite per indirizzo dovrebbe stare vicino al database che sa già se quell'indirizzo ha una conferma in sospeso. Il proxy dovrebbe invece gestire i limiti per IP e per endpoint, che sono il suo ambito naturale.
Non ripetere il testo inviato nel messaggio
Mantieni fuori dal messaggio che invii ogni stringa fornita dall'utente. Esistono due motivi distinti, ed entrambi sono già stati sfruttati in attacchi reali.
Se l'email di conferma saluta il destinatario usando un nome acquisito dal modulo, l'attaccante inserisce il proprio messaggio nel campo del nome. Il server invia quindi quel testo alla vittima dal tuo dominio, firmandolo con la tua chiave DKIM (DomainKeys Identified Mail). Il sito diventa così un servizio di invio per abusi commessi da terzi e il provider destinatario vede il tuo dominio associato al messaggio.
Il secondo motivo è più grave. Se un campo inviato viene concatenato manualmente in un'intestazione email, un carattere di nuova riga presente nel campo aggiunge intestazioni scelte dall'attaccante, inclusa Bcc. Le librerie email moderne rifiutano i caratteri di nuova riga nei valori delle intestazioni. Il codice che inoltra il testo a sendmail da uno script shell spesso non lo fa.
Un messaggio di conferma sicuro contiene il nome del sito e un link, con una sola frase esplicativa. L'indirizzo compare esclusivamente nel punto in cui ne ha bisogno il mail transfer agent, nell'intestazione To. Esegui questo test: invia il modulo con un campo del nome che contiene una nuova riga e un link evidente, quindi leggi il messaggio ricevuto in formato non elaborato con less e verifica che nessuno dei due elementi sia rimasto.
Controlla anche che la pagina di esito positivo mostri lo stesso messaggio per ogni indirizzo. Una pagina che per un indirizzo indica "sei già iscritto" e per un altro "controlla la posta in arrivo" trasforma il modulo in uno strumento per verificare l'appartenenza a una lista, a disposizione di chiunque abbia un elenco di indirizzi da testare.
Quale controllo anti-bot utilizzare?
Scegli il controllo con la stessa attenzione per l’accessibilità e per l’efficacia. Un CAPTCHA basato sulla selezione di immagini non può essere risolto da una persona non vedente, mentre il fallback audio è difficile da usare per chi ha capacità uditive nella norma. Se un controllo impedisce a una persona legittima di completare la registrazione, la difesa introduce anche un costo. Ecco quattro opzioni, nell’ordine in cui provarle.
Proof of work nel browser. Il browser calcola un hash che il server può verificare a basso costo; la persona non deve risolvere nulla. listmonk offre questa funzione in Settings, quindi Security, tramite ALTCHA, che non richiede servizi di terze parti. Ad agosto 2026 è la soluzione raccomandata da listmonk rispetto all’opzione hCaptcha deprecata. Il costo ricade su chi invia il maggior numero di richieste, cioè l’attaccante.
Un controllo gestito non interattivo. Cloudflare Turnstile non mostra quasi nulla alla maggior parte dei visitatori e presenta una verifica solo quando i suoi segnali indicano un comportamento sospetto. È efficace, ma inserisce un servizio di terze parti nel flusso di registrazione.
Un campo honeypot. È un campo di testo che la persona non vede, ma che un bot semplice compila. Assegnagli un nome che il modulo non utilizza altrimenti e imposta autocomplete="off", tabindex="-1" e aria-hidden="true", in modo che un password manager non lo compili e uno screen reader non lo annunci. Un campo denominato email2 o address viene compilato automaticamente dal browser; in questo caso rifiuteresti gli utenti reali.
<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 controllo sul tempo di invio. Inserisci un timestamp firmato in un campo nascosto quando viene visualizzata la pagina e rifiuta una richiesta inviata meno di due secondi dopo. Una persona non può leggere un modulo e digitare un indirizzo in così poco tempo. Firma il timestamp, altrimenti il bot invierà semplicemente un valore precedente.
Indipendentemente dall’opzione scelta, verifica un aspetto: il token deve essere consumato una sola volta. Se uno script può superare il controllo una volta e riutilizzare quel token per mille indirizzi, il controllo ha dimostrato soltanto che un browser è stato eseguito una volta, e nulla di più.
Come accorgersene prima che arrivi la segnalazione di abuso?
Devono essere i tuoi grafici a segnalartelo, non l'ufficio abusi del provider di hosting. Monitora due elementi.
Conta gli invii per indirizzo sorgente nell'intero log:
sudo awk '/POST \/subscription\/form/ {print $1}' /var/log/nginx/access.log \
| sort | uniq -c | sort -rn | head -20Poi fai in modo che fail2ban legga le stesse righe limiting requests già scritte da nginx e blocchi i responsabili delle ripetizioni. fail2ban include un filtro specifico per questo caso. Crea /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-reqL'output dello stato mostra il filtro della jail e i conteggi correnti dei tentativi falliti e degli indirizzi bloccati. Currently banned: 0 in una giornata tranquilla è corretto. Se la jail non compare affatto, fail2ban non ha caricato il file e sudo fail2ban-client -d | grep nginx-limit-req mostra la configurazione effettivamente analizzata. Il filtro incluso corrisponde a ogni zona limit_req. Limitalo alla zona di iscrizione impostando ngx_limit_req_zones = signup in una sezione [Definition] di /etc/fail2ban/filter.d/nginx-limit-req.local. La struttura dei file della jail e i comandi di blocco sono descritti più in dettaglio nella guida a fail2ban per Ubuntu 24.04.
Il secondo indicatore è un rapporto e non richiede nuovo software: invii divisi per conferme. In una lista sana, la maggior parte delle persone che invia un indirizzo fa clic sul link, in genere ben oltre la metà. Quando questo rapporto crolla mentre gli invii aumentano, qualcuno sta usando il servizio per i propri scopi. Confronta il numero di iscritti unconfirmed creati nell'ultima ora con il numero di quelli confirmed, usando la pianificazione con cui già esegui i report.
Il costo reale: reputazione del mittente e blocklist
Questa è la parte che trasforma un inconveniente in un costo.
Gli elenchi di indirizzi usati per l'invio massivo vengono raccolti automaticamente e contengono spamtrap: indirizzi che non si sono mai registrati a nulla e che vengono pubblicati soltanto per individuare i mittenti che inviano messaggi senza autorizzazione. Il messaggio di conferma raggiunge uno di questi indirizzi. Per alcuni gestori di blocklist è sufficiente questo.
I destinatari che non hanno richiesto il messaggio non fanno clic su "annulla iscrizione". Fanno clic su "segnala come spam". Le regole di Google per i mittenti che inviano grandi volumi, in vigore da febbraio 2024, richiedono ai mittenti che inviano almeno 5,000 messaggi al giorno a Gmail di mantenere in Postmaster Tools un tasso di segnalazioni come spam inferiore allo 0.3%. Un mittente con volumi inferiori non viene valutato rispetto a quel valore, ma lo stesso segnale relativo ai reclami alimenta le decisioni di filtraggio che spostano i messaggi nella cartella spam. Anche gli indirizzi falsi inclusi nell'esecuzione generano molti hard bounce e un aumento di questo tasso costituisce un ulteriore segnale negativo per la reputazione presso ogni grande provider.
Se gestisci il tuo server di posta su un VPS con mailcow, la presenza nella blocklist riguarda l'indirizzo IP e il dominio. La rimozione da una blocklist gestita da un operatore come Spamhaus richiede la compilazione di un modulo e un'attesa. Durante l'attesa, non vengono consegnati neppure i tuoi avvisi di fatturazione e i messaggi per la reimpostazione delle password. Se invece invii i messaggi tramite un provider condiviso, è probabile che sospenda prima il tuo account e legga solo dopo la tua spiegazione, perché il tuo traffico rappresenta un rischio per tutti gli altri mittenti che usano quell'indirizzo IP.
A fronte di questo rischio, il lavoro è limitato. Attiva oggi il confirmed opt-in, perché richiede una sola impostazione per elenco. Aggiungi poi il rate limit sul proxy, perché richiede un solo file e un reload. Il controllo dei bot e il monitoraggio degli avvisi possono essere aggiunti nel corso di questa settimana.
FAQ
Il double opt-in impedisce il subscription bombing?
Impedisce che la lista venga inquinata e limita il tuo contributo a un solo messaggio per ogni indirizzo inviato. È il miglioramento singolo più importante che puoi ottenere. Non impedisce che la casella della vittima si riempia, perché l'attacco consiste nella somma di un messaggio proveniente da ciascuno di mille siti. Usalo insieme a un rate limit per IP sul proxy e a un limite per i nuovi invii di conferma, in modo che l'invio dello stesso indirizzo per la seconda volta non generi un secondo messaggio.
Come distinguo una campagna di bombing da una giornata con molte iscrizioni reali?
Osserva cosa succede dopo l'invio. Gli utenti reali confermano l'iscrizione, di solito entro poche ore. Una campagna di bombing lascia molti indirizzi che non confermano mai, non aprono mai i messaggi e non fanno mai clic. Anche gli invii presentano una distribuzione anomala: molti indirizzi sorgente mai osservati prima, domini dei destinatari a cui di solito non invii messaggi e orari di arrivo distribuiti uniformemente nell'arco della giornata, invece di seguire gli orari in cui il tuo pubblico è normalmente attivo.
Devo eliminare gli indirizzi inviati?
Sì. Elimina i record non confermati più vecchi di circa trenta giorni e fallo secondo una pianificazione automatica, non manualmente. Non inviare mai altro a questi indirizzi, nemmeno un messaggio di scuse o del tipo "sei stato tu?", perché sarebbe un secondo messaggio non richiesto per una persona che ne ha già ricevuti moltissimi. Se tra questi indirizzi ci sono spamtrap, un messaggio successivo è la conferma che l'operatore della blocklist sta aspettando.
Il rate limiting rifiuterà gli iscritti reali?
Un limite per IP di un invio ogni trenta secondi, con un burst di tre, è impercettibile per una persona che compila un modulo una sola volta. Diventa visibile quando molte persone condividono lo stesso indirizzo, ad esempio in un ufficio dietro un unico gateway NAT (network address translation), oppure quando il proxy rileva l'indirizzo della CDN invece di quello del visitatore. Leggi $remote_addr nel log degli accessi prima di rendere il limite più restrittivo e mantieni il limite massimo dell'endpoint al di sopra del volume della tua ora di maggiore attività reale.
Il mio IP di invio è finito in una blocklist dopo una campagna. Cosa devo fare prima?
Interrompi gli invii da quell'IP prima di richiedere qualsiasi intervento. Metti in pausa la coda della campagna, correggi il modulo ed elimina gli indirizzi non confermati, perché una rimozione dalla blocklist seguita da altro traffico dello stesso tipo causa un nuovo inserimento più rapidamente rispetto alla prima volta. Individua quindi la lista in cui compari, poiché la maggior parte degli operatori dispone di una pagina di ricerca basata sul tuo indirizzo IP, e segui la procedura di rimozione. Considera che l'attesa può durare diversi giorni e usa questo tempo per verificare che il record SPF (sender policy framework) e la firma DKIM superino ancora i relativi controlli.