Le contexte
Le site vitrine d'un cabinet de conseil, en Symfony 8, encaisse une campagne automatisee : plus de 2 000 messages deposes via le formulaire de contact, tous en cyrillique, du type У вас поступил перевод 51068 руб. suivi d'un lien.
Reflexe immediat : purge de la table, mise en place d'un rate limiter. Et pourtant, les messages undelivered continuaient d'arriver dans la boite du cabinet.
Le symptome qui met sur la piste
Les rebonds arrivaient sur l'adresse du cabinet, alors que personne n'avait rien envoye depuis cette boite. C'est ce detail qui a tout debloque : ce n'etait pas l'attaquant qui spammait la boite, c'etait le site lui-meme.
Pourquoi ca casse
Le formulaire envoyait deux e-mails a chaque soumission : une notification interne, et un accuse de reception a l'adresse saisie par le visiteur. Les robots remplissaient ce champ avec des adresses inventees. Le serveur envoyait donc 2 000 courriels vers des boites inexistantes, et recevait 2 000 rebonds en retour.
C'est du backscatter : le site devient un relais de nuisance. Le rate limiter n'y change rien, les envois sont deja partis. Pire, ces rebonds degradent la reputation d'envoi du domaine, avec un risque reel de finir en spam chez les vrais prospects.
Correctif 1 : ne repondre qu'aux domaines qui existent
Avant d'envoyer un accuse de reception, on verifie que le domaine declare un serveur de messagerie. La notification interne, elle, part toujours : aucune demande legitime n'est perdue.
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');
}
Correctif 2 : filtrer le contenu, pas seulement le debit
Deuxieme constat : les expediteurs utilisaient de vraies adresses Gmail. Le controle MX passe donc sans probleme. Seul un filtre de contenu pouvait les arreter.
Le cabinet ne travaille qu'en francais et en anglais. Toute lettre hors alphabet latin fait rejeter la soumission, sans casser les accents francais ni les noms espagnols ou allemands :
public function hasNonLatinScript(string $text): bool
{
    return 1 === preg_match('/\p{L}/u', $text)
        && 1 === preg_match('/(?!\p{Latin})\p{L}/u', $text);
}
Le reste fonctionne par score : un lien est tolere, deux suffisent a rejeter ; les balises HTML ou BBCode dans un champ de formulaire valent un rejet direct ; une liste de mots-cles de campagne ajoute des points.
Detail qui compte : une soumission rejetee recoit exactement le meme message de succes qu'une demande legitime. Rien n'est enregistre, aucun e-mail ne part, et le robot ne peut pas deduire qu'il a ete bloque pour ajuster son contenu.
Correctif 3 : durcir le formulaire
  • Delai minimal de remplissage. Le formulaire embarque son heure d'affichage ; un envoi en moins de quatre secondes n'est pas humain.
  • Numeros composables. 8 a 15 chiffres, indicatif international facultatif. Cela ecarte les saisies fantaisistes, pas les faux plausibles.
  • Limites resserrees. 3 envois par quart d'heure et 5 par jour et par IP.
  • Honeypot deja en place, conserve.
Resultats
  • Message cyrillique avec lien : 0 e-mail, non enregistre.
  • Envoi instantane : 0 e-mail, non enregistre.
  • Numero invalide : refuse en 422.
  • Demande legitime francaise : enregistree, 2 e-mails envoyes.
  • Message legitime contenant un lien : accepte, pas de faux positif.
Le piege du nettoyage en base
La requete evidente pour reperer le spam matche tous les accents francais : un message de « Awa Diop, societe basee a Dakar » est compte comme du 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 '[Ѐ-ӿ]';
Ce que je retiens
  • Ne jamais repondre automatiquement a une adresse non verifiee. C'est la porte ouverte au backscatter, et c'est la reputation de ton domaine qui trinque.
  • Un rate limiter ne repare pas ce qui est deja parti. Il limite le debit, il ne filtre pas le contenu.
  • Le symptome n'est pas la cause. Les rebonds venaient de mon propre serveur. Sans cette lecture, j'aurais durci le rate limiter indefiniment sans jamais arreter les undelivered.