L’essentiel en 30 secondes
- Un jeton CSRF par session bloque les envois forgés depuis d’autres sites.
- Un champ piège et un délai minimal suffisent à écarter la majorité des robots.
- Toute donnée reçue est validée côté serveur, même si le navigateur l’a déjà vérifiée.
- Aucun retour à la ligne ne doit atteindre les en-têtes de l’e-mail.
Le formulaire de contact est souvent la porte la plus exposée d’un site vitrine : il accepte du texte libre, envoie des e-mails et reste accessible à tout le monde, robots compris. Bonne nouvelle, six réflexes simples suffisent à le rendre solide, sans captcha agaçant pour vos visiteurs.
1. Un jeton CSRF pour chaque session
Une attaque CSRF consiste à faire envoyer votre formulaire par le navigateur d’un visiteur, depuis un autre site. La parade : générer un jeton aléatoire, le placer dans un champ caché, puis vérifier qu’il correspond à celui stocké en session.
session_start();
// à l'affichage du formulaire
$_SESSION['csrf'] ??= bin2hex(random_bytes(32));
// à la réception
$token = $_POST['csrf'] ?? '';
if (!is_string($token) || !hash_equals($_SESSION['csrf'], $token)) {
http_response_code(400);
exit('Session expirée, merci de recharger la page.');
}2. Un champ piège et un délai minimal
Les robots remplissent tous les champs qu’ils trouvent, et vite. On ajoute donc un champ masqué aux humains (le « pot de miel ») et on refuse les envois réalisés moins de trois secondes après l’affichage de la page.
<div class="hp" aria-hidden="true">
<label for="website">Ne pas remplir</label>
<input id="website" name="website" tabindex="-1" autocomplete="off">
</div>
<input type="hidden" name="ts" value="<?= time() ?>">$isBot = ($_POST['website'] ?? '') !== ''
|| time() - (int) ($_POST['ts'] ?? 0) < 3;
if ($isBot) {
// on simule un succès : le robot n'apprend rien
header('Location: merci.php');
exit;
}3. Valider et normaliser côté serveur
Les attributs required ou type="email" améliorent le confort, mais ils se contournent en une ligne. Le serveur doit tout revérifier : présence, type, longueur et format.
$name = trim((string) ($_POST['name'] ?? ''));
$email = filter_var($_POST['email'] ?? '', FILTER_VALIDATE_EMAIL);
$message = trim((string) ($_POST['message'] ?? ''));
$errors = [];
if ($name === '' || mb_strlen($name) > 80) {
$errors['name'] = 'Merci d’indiquer votre nom (80 caractères max).';
}
if ($email === false) {
$errors['email'] = 'Cette adresse e-mail semble invalide.';
}
if (mb_strlen($message) < 20 || mb_strlen($message) > 5000) {
$errors['message'] = 'Votre message doit faire entre 20 et 5 000 caractères.';
}- Plafonnez chaque champ : un message de 2 Mo n’a rien à faire dans votre boîte mail.
- Utilisez les fonctions
mb_*pour compter correctement les caractères accentués. - Pour les listes (motif, budget…), vérifiez que la valeur reçue fait partie des choix proposés.
4. Bloquer l’injection d’en-têtes e-mail
Si l’adresse de l’expéditeur ou l’objet contient un retour à la ligne, un attaquant peut ajouter ses propres en-têtes, par exemple Bcc:, et transformer votre serveur en relais de spam.
$clean = fn (string $v): string => str_replace(["\r", "\n", "%0a", "%0d"], '', $v);
$headers = [
'From' => 'Site Foxalia <no-reply@foxalia.fr>', // adresse fixe de votre domaine
'Reply-To' => $clean($email), // l'adresse du visiteur va ici
'Content-Type' => 'text/plain; charset=UTF-8',
];
mail('contact@foxalia.fr', $clean('Nouveau message de ' . $name), $message, $headers);5. Limiter le nombre d’envois
Même avec les protections précédentes, un script obstiné peut envoyer des centaines de messages. Une limite simple, par session ou par adresse IP, coupe court aux abus sans gêner un visiteur normal.
$now = time();
$hits = array_filter($_SESSION['sent'] ?? [], fn (int $t) => $t > $now - 3600);
if (count($hits) >= 5) {
$errors['form'] = 'Trop d’envois récents, merci de réessayer dans une heure.';
}
$_SESSION['sent'] = [...$hits, $now];6. Échapper à l’affichage et soigner les erreurs
Quand le formulaire est réaffiché avec les valeurs saisies, chaque valeur doit passer par htmlspecialchars(). Sinon, un simple <script> dans le champ nom devient une faille XSS.
function e(string $value): string
{
return htmlspecialchars($value, ENT_QUOTES, 'UTF-8');
}
?>
<input name="name" value="<?= e($name) ?>"
aria-invalid="<?= isset($errors['name']) ? 'true' : 'false' ?>">Côté expérience, affichez les erreurs à côté du champ concerné, conservez ce qui a été saisi et ne révélez jamais de détail technique (chemin de fichier, message SMTP) dans la page.
La check-list à garder sous la main
- Jeton CSRF vérifié avec
hash_equals() - Champ piège hors écran et délai minimal de 3 secondes
- Validation serveur de chaque champ, longueurs plafonnées
- Aucun retour à la ligne dans les en-têtes, expéditeur fixe
- Limite d’envois par session ou par IP
- Valeurs réaffichées échappées, erreurs claires et sans détail technique
La meilleure protection anti-spam est celle que vos visiteurs ne remarquent jamais.
Principe appliqué sur tous les formulaires Foxalia
Ces six étapes protègent déjà la grande majorité des sites vitrines. Pour un formulaire plus exposé (devis, inscription, paiement), on ajoute une journalisation des tentatives et une vérification anti-robot côté serveur.
Commentaires
Aucun commentaire pour le moment. Soyez le premier à réagir !