PHP news

Sécuriser un formulaire de contact PHP en 6 étapes

Jeton CSRF, champ piège, validation, en-têtes d’e-mail : la check-list pour bloquer le spam et les injections.

FoxaliaDéveloppeur & fondateur 6 min de lecture Mis à jour le

Article pilote : ce contenu sert de démonstration de mise en page en attendant les vrais articles.

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.

PHPGénération et vérification du jeton
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.

HTML
<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() ?>">
PHP
$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.

PHP
$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.

PHP
$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.

PHP
$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.

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

Laisser un commentaire

À lire aussi

Tous les articles

Besoin d’un formulaire ou d’un site vraiment sécurisé ?

Conseil, audit ou développement sur mesure : je vous réponds sous 48 h ouvrées.