Il contesto
Il sito vetrina di una società di consulenza, in Symfony 8, subisce una campagna automatizzata: oltre 2.000 messaggi inviati tramite il modulo di contatto, tutti in cirillico, del tipo У вас поступил перевод 51068 руб. seguito da un link.
Reazione immediata: svuotamento della tabella, implementazione di un rate limiter. Eppure, i messaggi undelivered continuavano ad arrivare nella casella della società.
Il sintomo che mette sulla pista
I rimbalzi arrivavano all'indirizzo della società, mentre nessuno aveva inviato nulla da quella casella. È questo dettaglio che ha sbloccato tutto: non era l'attaccante a spammare la casella, era il sito stesso.
Perché si rompe
Il modulo inviava due email a ogni invio: una notifica interna e una conferma di ricezione all'indirizzo inserito dal visitatore. I bot compilavano questo campo con indirizzi inventati. Il server inviava quindi 2.000 email verso caselle inesistenti e riceveva 2.000 rimbalzi in risposta.
È backscatter: il sito diventa un relais di molestie. Il rate limiter non cambia nulla, gli invii sono già partiti. Peggio ancora, questi rimbalzi degradano la reputazione di invio del dominio, con un rischio reale di finire in spam presso i veri potenziali clienti.
Correzione 1: rispondere solo ai domini che esistono
Prima di inviare una conferma di ricezione, verifichiamo che il dominio dichiari un server di posta. La notifica interna, invece, parte sempre: nessuna richiesta legittima va persa.
public function isDeliverable(?string $email): bool
{
if (null === $email || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
return false;
}
$domain = substr(strrchr($email, '@') ?: '', 1);
if ('' === $domain) {
return false;
}
// Resolveur indisponible : on n'envoie pas, plutot que de risquer un rebond.
return checkdnsrr($domain, 'MX') || checkdnsrr($domain, 'A');
}
Correzione 2: filtrare il contenuto, non solo la frequenza
Seconda constatazione: i mittenti usavano veri indirizzi Gmail. Il controllo MX passa quindi senza problemi. Solo un filtro dei contenuti poteva fermarli.
La società lavora solo in francese e in inglese. Qualsiasi lettera al di fuori dell'alfabeto latino fa rifiutare l'invio, senza rompere gli accenti francesi né i nomi spagnoli o tedeschi:
public function hasNonLatinScript(string $text): bool
{
return 1 === preg_match('/\p{L}/u', $text)
&& 1 === preg_match('/(?!\p{Latin})\p{L}/u', $text);
}
Il resto funziona a punteggio: un link è tollerato, due bastano a rifiutare; i tag HTML o BBCode in un campo del modulo valgono un rifiuto immediato; una lista di parole chiave di campagna aggiunge punti.
Dettaglio che conta: un invio rifiutato riceve esattamente lo stesso messaggio di successo di una richiesta legittima. Nulla viene registrato, nessuna email parte, e il bot non può dedurre di essere stato bloccato per adattare il suo contenuto.
Correzione 3: rafforzare il modulo
- Ritardo minimo di compilazione. Il modulo incorpora l'ora di visualizzazione; un invio in meno di quattro secondi non è umano.
- Numeri componibili. Da 8 a 15 cifre, prefisso internazionale facoltativo. Questo scarta gli inserimenti fantasiosi, non i falsi plausibili.
- Limiti più stretti. 3 invii ogni quarto d'ora e 5 al giorno per IP.
- Honeypot già in place, mantenuto.
Risultati
- Messaggio cirillico con link: 0 email, non registrato.
- Invio istantaneo: 0 email, non registrato.
- Numero non valido: rifiutato con 422.
- Richiesta legittima francese: registrata, 2 email inviate.
- Messaggio legittimo contenente un link: accettato, nessun falso positivo.
La trappola della pulizia nel database
La query ovvia per individuare lo spam intercetta tutti gli accenti francesi: un messaggio di « Awa Diop, société basée à Dakar » viene conteggiato come spam.
-- Trop large : attrape aussi les accents
SELECT COUNT(*) FROM contact_request WHERE message REGEXP '[^\x00-\x7F]';
-- Cible le bloc cyrillique uniquement
SELECT COUNT(*) FROM contact_request WHERE message REGEXP '[Ѐ-ӿ]';
Cosa ne ricavo
- Non rispondere mai automaticamente a un indirizzo non verificato. È la porta aperta al backscatter, ed è la reputazione del tuo dominio che ne risente.
- Un rate limiter non ripara ciò che è già partito. Limita la frequenza, non filtra il contenuto.
- Il sintomo non è la causa. I rimbalzi venivano dal mio stesso server. Senza questa lettura, avrei rafforzato il rate limiter all'infinito senza mai fermare gli undelivered.