Formulaire anti‑spam

Un chevalier en armure dorée avec bouclier marqué JS protège un formulaire contre les spams
Protection anti‑spam

Comment coder un formulaire anti‑spam sans captcha

Projet : Sécuriser la réception d'emails

Bootstrap | PHP | Javascript

2022
fev
code

J'ai réuni ici le résultat de mes recherches concernant les bonnes pratiques à utiliser lors de la création d'un formulaire de contact avec envoi d'email.

  • Technique du "pot de miel" (honeypot)
  • Génération du html en vanilla javascript
  • Filtrage des liens et mots-clés côté serveur en php

Réalisation :

Les techniques décrites ci-dessous m'ont permis de me débarrasser du spam.

Introduction

Même si la majeure partie du spam (près de 90%) est filtrée en amont par les outils anti-spam des fournisseurs d’accès internet et de messagerie, il semble que 10% environ finit par atterrir dans votre boîte de réception.

Ainsi, lorsque j'ai mis en ligne la première version de ma page de contact, je me suis très vite retrouvé envahi par des spams divers et variés.

Mettre en place un captcha ?

Je ne sais pas si vous partagez mon sentiment, mais je trouve les captchas particulièrement pénibles à utiliser lorsque je dois valider un formulaire sur un site web.

Souhaitant offrir à l'utilisateur une expérience fluide, j'ai choisi de me passer de captcha et j'ai mis en place 3 couches de protection simples qui se sont révélées efficaces, sachant qu'on parle ici d'un formulaire basique.

Formulaire Bootstrap 5

Préparation du projet :

Créer un dossier avec 4 fichiers

dossier formulaire anti spam

Un formulaire de contact Bootstrap simple dans le fichier contact.php :

📝 Note : On peut éviter de préciser l'attribut' "label for=" si on inclut le input dans le label

Voici le résultat :

Technique du "pot de miel"

Mille mercis à Benoit de nouvelle-techno.fr qui a réalisé un excellent tutoriel et une vidéo sur le sujet.

Citons l'auteur :

La technique du pot de miel : comment ça marche ?

La plupart des spams sont envoyés par des robots, scripts automatiques qui parcourent les sites à la recherche de formulaires. Pour un formulaire de contact, les robots se sentent obligés de remplir tous les champs proposés.

Le Honeypot consiste à insérer dans le formulaire un champ supplémentaire, masqué pour les visiteurs normaux. Ceux-ci ne le rempliront pas étant donné qu'ils ne le voient pas. Un robot, lui, le détectera et le complétera.

Ici nous allons masquer le champ "sujet" :

On pourrait utiliser <input type="hidden">

Bootstrap propose la classe .d-none pour masquer du contenu html,
ce qui revient à appliquer la propriété css display: none;

Mais si un spambot était suffisamment malin pour repérer <input type="hidden"> ou pour surveiller les classes standard de Bootstrap ou des classes nommées .hidden, .mask, .hide etc. ?

Nous pouvons tenter de déjouer la malveillance du robot en définissant une classe anodine .sujet qui aura la même fonction. Ajoutons la dans style.css et appliquons la aux label et input :

                    
                    /* style.css */
.sujet {
    display: none;
}
                    
                

📝 Note : On applique l'attribut required aux input mais pas pour le sujet qui ne doit pas être rempli.
On mettra aussi l'attribut autocomplete sur "off".

Traitement des données

Le fichier envoi.php va s'en occuper côté serveur en php

                    
// vérification de la présence des données
if (
    isset($_POST["name"]) && !empty($_POST["name"]) &&
    isset($_POST["email"]) && !empty($_POST["email"]) &&
    isset($_POST["message"]) && !empty($_POST["message"])
) {
    // nettoyage des données
    // strip_tags — Supprime les balises HTML et PHP d'une chaîne
    $nom = strip_tags($_POST["name"]);
    $email = strip_tags($_POST["email"]);
    // htmlspecialchars — Convertit les caractères spéciaux en entités HTML
    $message = htmlspecialchars($_POST["message"]);

    // Vérifier sujet : s'il est rempli, fin du script
    if (isset($_POST["sujet"]) && !empty($_POST["sujet"])) {
        $sujetSpam = strip_tags($_POST["sujet"]);
        die();
    }
    // Si le sujet n'a pas été rempli, on valide l'email et on continue :
    if (filter_var($_POST["email"], FILTER_VALIDATE_EMAIL)) {
        // wordwrap() pour limiter les lignes à 60 caractères
        $message = wordwrap($message, 60);
        // envoi de l'email
        $to = "votre-email";
        $retour = mail($to, "Envoi depuis votre page Contact : $nom", "message : " . $message, "From : " . $email);

        if ($retour) {
            echo "Merci {$nom}, Votre message a bien été envoyé";
        }

        if (!$retour) {
            echo "Désolé, {$nom}, la transmission de votre message a échoué";
        }
    } // validate form
} // isset

                    
                

Comment tester l'envoi d'email en local ?

Utilisez Laragon comme serveur local apache / php. Il dispose d'un intercepteur d'emails qui permet de vérifier vos envois et de les stocker.

intercepteur d'emails de Laragon

Générer le formulaire en javascript

On peut utiliser javascript pour apporter une couche supplémentaire de sécurité ; si les champs de formulaire ne sont pas présents sur la page mais sont générés à l'ouverture, peu de robots seront capables d'analyser le javascript comme ils le font pour le html.

Ajouter à la suite de ce code :

<script src="contact.js"></script>

pour charger le script :

                
                // script contact.js
addForm = function () {

    const formContainer = document.getElementById('form_container');
    const sendBtn = document.getElementById('sendBtn');

    const form = document.createElement('form');
    form.setAttribute('method', 'post');
    form.setAttribute('action', 'envoi.php');
    form.setAttribute('accept-charset', 'UTF-8');
    form.setAttribute('autocomplete', 'on');

    // name
    const label_name = document.createElement('label');
    label_name.setAttribute('for', 'name')
    label_name.setAttribute('class', 'form-label')
    label_name.insertAdjacentHTML('beforeend', 'Veuillez taper votre nom :');

    const input_name = document.createElement('input');
    input_name.setAttribute('name', 'name');
    input_name.setAttribute('placeholder', 'ex : jean');
    input_name.setAttribute('class', 'form-control');
    input_name.required = true;

    // email
    const label_email = document.createElement('label');
    label_email.setAttribute('for', 'email');
    label_email.setAttribute('class', 'form-label')
    label_email.insertAdjacentHTML('beforeend', 'Veuillez taper votre adresse email :');

    const input_email = document.createElement('input');
    input_email.setAttribute('name', 'email');
    input_email.setAttribute('type', 'email');
    input_email.setAttribute('pattern', '[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,4}$');
    input_email.setAttribute('placeholder', 'ex : jean@jiggle.com');

    input_email.setAttribute('value', 'contact@jpg-design.com');

    input_email.setAttribute('class', 'form-control');
    input_email.required = true;

    // sujet
    const label_sujet = document.createElement('label');
    label_sujet.setAttribute('for', 'sujet');
    label_sujet.insertAdjacentHTML('beforeend', 'Veuillez taper votre sujet :');
    label_sujet.setAttribute('class', 'form-label sujet');

    const input_sujet = document.createElement('input');
    input_sujet.setAttribute('name', 'sujet');
    input_sujet.setAttribute('placeholder', 'ex : sujet');
    input_sujet.setAttribute('class', 'form-control sujet');
    // propriétés spécifiques au champ caché :
    // Enlever l'autocomplétion
    input_sujet.setAttribute('autocomplete', 'off');
    // Enlever l'attribut required
    input_sujet.required = false;

    // message
    const label_msg = document.createElement('label');
    label_msg.setAttribute('for', 'message');
    label_msg.setAttribute('class', 'form-label')
    label_msg.insertAdjacentHTML('beforeend', 'Veuillez taper votre message :');

    const input_msg = document.createElement('textarea');
    input_msg.setAttribute('name', 'message');
    input_msg.setAttribute('placeholder', 'Votre message ici... (Les liens seront traités comme spam)');
    input_msg.setAttribute('class', 'form-control');
    input_msg.setAttribute('required', '');

    // add form
    formContainer.appendChild(form);
    // add inputs
    form.appendChild(label_name);
    form.appendChild(input_name);

    form.appendChild(label_email);
    form.appendChild(input_email);

    form.appendChild(label_sujet);
    form.appendChild(input_sujet);

    form.appendChild(label_msg);
    form.appendChild(input_msg);

    form.appendChild(sendBtn);
}

// test loading page
if (window.addEventListener) {
    window.addEventListener("load", addForm, false);
}
            
        

Filtrage des liens et mots-clés

Pour filtrer les liens on va utiliser une expression régulière

Pour filtrer les mots-clés on va créer un tableau rempli de ce qu'on veut éviter, puis le parcourir à la recherche d'un mot-clé typique d'un spam.

                    
...
    // Vérifier la présence de liens : si oui, fin du script
    if ( preg_match('/http(s?):\/\//ism',$message) ) {

        die();
    }

    // Vérifier la présence de mots-clés
    $bads = array('casino','gambling','ranks','Bitcoin','viagra','v i a g r a');
    // boucle pour parcourir le tableau de bad words
    foreach ($bads as $bad) {
        // stripos — Recherche la position de la première occurrence dans une chaîne, sans tenir compte de la casse.
        // si la recherche dans $message donne un résultat, fin du script
        if ( stripos($message, $bad) !== false ) {

            break;
            die();
        }	
    }
    // Si toutes les conditions sont remplies, envoi de l'email
    if ( filter_var($_POST["email"], FILTER_VALIDATE_EMAIL) && stripos($message, $bad) === false ) {

        $message = wordwrap($message,60);
        // envoi de l'email
        $to = "votre-email";
        $retour = mail($to, "Envoi depuis votre page Contact : $nom", "message : " . $message, "From : " . $email);
...
                    
                

Téléchargement du zip contenant le projet complet

Sur le thème code voir aussi :