Protection de base pour un formulaire
L’envoi de formulaires via PHP est une fonctionnalité omniprésente sur les sites web. Un simple formulaire de contact, une demande de devis ou une inscription à une newsletter : ces éléments semblent anodins, mais ils sont souvent la cible de tentatives d’abus.
Un formulaire non sécurisé devient vite un point d’entrée pour les robots spammeurs. Ces derniers peuvent inonder votre boîte e-mail de messages vides, répétitifs, ou même malveillants. Pire encore, un formulaire vulnérable peut être exploité pour envoyer du spam à votre insu, risquant ainsi de faire blacklister votre serveur.
Pour éviter ces désagréments, nous allons bâtir un formulaire sécurisé, en ajoutant progressivement plusieurs couches de protection. L’idée est simple : nous identifions les attaques possibles et nous les neutralisons, une par une.
Mise en place d’un formulaire de base
Avant d’aborder les mesures de sécurité, commençons par la base : un formulaire simple permettant aux utilisateurs d’envoyer un message par e-mail. Ce formulaire comportera trois champs essentiels : le Nom, pour identifier l’expéditeur, l’E-mail, afin de pouvoir lui répondre, et le Message, où il pourra formuler sa requête. Enfin, un bouton Envoyer permettra de soumettre ces informations en un clic.
<form id="contactForm" action="send.php" method="POST">
<label for="name">Nom :</label>
<input type="text" id="name" name="name" required>
<label for="email">Email :</label>
<input type="email" id="email" name="email" required>
<label for="message">Message :</label>
<textarea id="message" name="message" required></textarea>
<button type="submit">Envoyer</button>
</form>
Lorsque l’utilisateur clique sur « Envoyer », les données sont transmises au fichier send.php
, qui se charge de l’envoi du mail.
Script send.php
coté serveur
Pour envoyer l’e-mail, nous allons utiliser PHPMailer, un outil puissant qui nous permet d’envoyer des mails de manière sécurisée via un serveur SMTP. N’hésitez pas le cas échéant de vous rapprocher de notre précédent article Maîtriser l’envoi d’e-mails avec PHPMailer.
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\Exception;
require 'vendor/autoload.php';
if ($_SERVER["REQUEST_METHOD"] == "POST") {
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = 'smtp.example.com';
$mail->SMTPAuth = true;
$mail->Username = 'user@example.com';
$mail->Password = 'password';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom($_POST['email'], $_POST['name']);
$mail->addAddress('contact@monsite.com');
$mail->isHTML(true);
$mail->Subject = 'Message depuis le formulaire';
$mail->Body = nl2br(htmlspecialchars($_POST['message']));
$mail->send();
echo "Message envoyé avec succès !";
} catch (Exception $e) {
echo "Erreur : " . $mail->ErrorInfo;
}
}
?>
À ce stade, notre formulaire fonctionne et permet déjà d’envoyer des e-mails, mais il reste vulnérable à différentes attaques. Un utilisateur malveillant ou un robot pourrait facilement exploiter ses failles pour inonder votre boîte de réception de messages indésirables, voire détourner votre script pour envoyer du spam en masse. Pour éviter ces risques et renforcer la sécurité, nous allons progressivement ajouter plusieurs couches de protection. Passons maintenant à la mise en place de ces défenses.
Vérification du Referer pour bloquer les requêtes externes
Le champ HTTP_REFERER
est une variable envoyée par le navigateur lors d’une requête HTTP. Il indique l’URL de la page d’origine depuis laquelle la requête a été envoyée. Cette information permet d’identifier si une requête provient bien de notre propre site ou si elle a été générée depuis un site externe ou un script malveillant.
Dans notre cas, nous allons utiliser HTTP_REFERER
pour bloquer les requêtes qui ne proviennent pas de notre propre domaine. Ainsi, nous nous protégeons contre les attaques où un script externe tenterait de soumettre directement des requêtes à notre fichier send.php
sans passer par notre formulaire légitime.
if (!isset($_SERVER['HTTP_REFERER']) || parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) !== $_SERVER['HTTP_HOST']) {
die("Erreur : Requête suspecte.");
}
$_SERVER['HTTP_REFERER']
: récupère l’URL d’origine de la requête.isset($_SERVER['HTTP_REFERER'])
: vérifie que cette variable est bien définie. Si elle est absente, il est possible que la requête ait été envoyée directement par un script malveillant.parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) !== $_SERVER['HTTP_HOST']
: extrait le domaine de l’URL référente et le compare avec notre propre domaine ($_SERVER['HTTP_HOST']
). Si les deux ne correspondent pas, cela signifie que la requête provient d’un site externe.die("Erreur : Requête suspecte.");
: si la requête est suspecte, elle est immédiatement stoppée.
Cette vérification permet donc d’éviter certaines attaques automatisées, bien que HTTP_REFERER
ne soit pas toujours fiable à 100 % (il peut être modifié ou absent dans certains navigateurs). Il s’agit néanmoins d’un bon premier niveau de protection.
Enregistrement des tentatives suspectes
Bloquer les requêtes suspectes, c’est bien. Mais comprendre d’où elles viennent, c’est encore mieux. En enregistrant les tentatives échouées, nous pouvons analyser des schémas d’attaque et affiner nos mesures de protection pour mieux anticiper les menaces.
Pour cela, nous allons mettre en place un système de journalisation des tentatives suspectes. Chaque requête rejetée sera consignée avec les informations suivantes :
- La date et l’heure de la tentative, pour suivre les moments où les attaques surviennent le plus fréquemment.
- L’adresse IP de l’expéditeur, afin d’identifier d’éventuelles attaques répétées depuis la même source.
- Le motif du rejet, permettant d’analyser quelles failles sont le plus souvent ciblées et d’adapter nos défenses en conséquence.
Grâce à ces informations, nous pourrons mieux comprendre la nature des attaques et prendre les mesures nécessaires pour renforcer la sécurité de notre formulaire. Ajoutons cette fonction dans send.php
:
$logFile = 'attempts.log';
function logAttempt($reason) {
global $logFile;
$entry = date('Y-m-d H:i:s') . " - IP: " . $_SERVER['REMOTE_ADDR'] . " - Motif: $reason\n";
file_put_contents($logFile, $entry, FILE_APPEND);
}
Nous intégrons ensuite cet appel après chaque tentative de rejet, garantissant ainsi que chaque action suspecte est bien enregistrée et pourra être analysée ultérieurement.
if (!isset($_SERVER['HTTP_REFERER']) || parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST) !== $_SERVER['HTTP_HOST']) {
logAttempt("Requête suspecte (Referer invalide)");
die("Erreur : Requête suspecte.");
}
Désormais, chaque tentative suspecte est enregistrée, ce qui nous permet non seulement de repérer des attaques en cours, mais aussi d’anticiper les prochaines stratégies frauduleuses. Grâce à cette base de données d’incidents, nous pourrons analyser les tendances et renforcer nos mesures de sécurité en fonction des méthodes employées par les attaquants. Dans les sections suivantes, nous explorerons d’autres techniques pour piéger les robots et ralentir les tentatives d’intrusion.
Ajout d’un Token CSRF pour renforcer la sécurité
L’utilisation du HTTP_REFERER
pour vérifier l’origine d’une requête peut être utile, mais elle n’est pas toujours fiable à 100 %. Certains navigateurs ou proxys peuvent ne pas transmettre cette information, et un attaquant expérimenté pourrait la falsifier. Pour pallier ces limites et renforcer encore la sécurité de notre formulaire, nous allons mettre en place un token CSRF (Cross-Site Request Forgery).
Un token CSRF est une valeur unique générée côté serveur et stockée dans une session. Cette valeur est ensuite transmise avec le formulaire et comparée lors de la soumission. Si le token ne correspond pas, la requête est bloquée.
Génération du token côté serveur
Dans notre fichier form.php
, avant d’afficher le formulaire, nous devons générer un token unique et l’enregistrer dans la session :
session_start();
if (!isset($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
Ensuite, nous ajoutons ce token dans le formulaire sous forme de champ caché :
<input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
Vérification du token lors de l’envoi
Dans send.php
, nous devons vérifier que le token soumis correspond bien à celui stocké en session :
session_start();
if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token']) {
logAttempt("Token CSRF invalide");
die("Erreur : Requête invalide.");
}
session_start();
: Initialise la session pour accéder aux variables enregistrées.$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
: Génère un token unique si aucun n’existe déjà.- Le champ caché dans le formulaire permet d’envoyer ce token avec la requête.
- Lors de la soumission, nous comparons le token reçu avec celui stocké en session.
- Si les valeurs ne correspondent pas, nous considérons que la requête est suspecte et nous la bloquons.
L’ajout d’un token CSRF est une solution simple et efficace pour protéger notre formulaire contre les attaques par requêtes intersites et renforce la fiabilité des vérifications déjà mises en place avec HTTP_REFERER
.
Ajout d’un Honey Pot pour piéger les robots
Les robots spammeurs remplissent souvent tous les champs d’un formulaire, même ceux qui sont invisibles pour un utilisateur classique. Ce comportement automatique peut être exploité pour les identifier et les bloquer avant qu’ils ne soumettent une requête indésirable.
Pour cela, nous allons ajouter un champ caché dans notre formulaire, que seuls les robots verront et rempliront. Un utilisateur humain, en revanche, ne pourra pas l’apercevoir et donc ne le renseignera pas. Ainsi, si ce champ contient une valeur à la soumission du formulaire, nous saurons immédiatement qu’il ne s’agit pas d’un utilisateur légitime, mais d’un robot essayant d’abuser de notre système.
Il est important de noter que ce champ ne doit pas être un input type="hidden"
, car certains robots ignorent ces champs. À la place, nous utilisons un input type="text"
, rendu invisible pour l’utilisateur grâce à une règle CSS. Cela peut être fait de deux manières : soit via un style inline (style="display: none;"
), soit en appliquant une classe CSS (.honeypot { display: none; }
). Dans les deux cas, un robot, qui remplit souvent tous les champs visibles sans distinction, sera piégé en renseignant cette valeur, alors qu’un utilisateur légitime ne le fera pas. Dans form.php
, ajoutons un champ invisible :
<input type="text" name="website" id="website" style="display:none;">
Dans send.php
, nous allons vérifier si le champ caché a été rempli. Comme ce champ est invisible pour un utilisateur normal, sa présence dans la requête indique presque à coup sûr qu’un robot a tenté de soumettre le formulaire. Voici comment nous procédons :
if (!empty($_POST['website'])) {
logAttempt("Honey Pot rempli (robot détecté)");
die("Erreur : Robot détecté.");
}
$_POST['website']
: récupère la valeur soumise par le champ caché du formulaire.!empty($_POST['website'])
: vérifie si le champ a été rempli.logAttempt("Honey Pot rempli (robot détecté)")
: enregistre la tentative suspecte dans le fichier de logs.die("Erreur : Robot détecté.")
: bloque immédiatement la requête frauduleuse et empêche l’envoi de l’e-mail.
Ainsi, toute tentative d’un robot sera interceptée avant même d’atteindre le script d’envoi du mail, améliorant considérablement la protection du formulaire.
Test de rapidité d’envoi
Un humain met généralement quelques secondes à remplir un formulaire, car il prend le temps de lire les champs et de taper son message. En revanche, un robot peut soumettre une requête en une fraction de seconde, sans aucune pause. Cette rapidité anormale est un indicateur clé d’une tentative automatisée.
Pour détecter ces comportements suspects, nous allons enregistrer l’instant précis où le formulaire est chargé et comparer ce temps avec celui de l’envoi. Si la durée entre ces deux événements est trop courte, nous considérerons la requête comme suspecte et la bloquerons immédiatement. Cela nous permettra d’écarter une grande partie des envois effectués par des scripts malveillants.
Dans form.php
, ajoutons un champ caché rempli en JavaScript :
<input type="hidden" id="formStartTime" name="formStartTime">
<script>document.getElementById('formStartTime').value = Date.now();</script>
Dans send.php
,
if (isset($_POST['formStartTime'])) {
$duration = time() - (int) ($_POST['formStartTime'] / 1000);
if ($duration < 3) {
logAttempt("Envoi trop rapide ($duration s)");
die("Erreur : Envoi trop rapide.");
}
}
Avant d’exploiter la variable formStartTime
, nous devons vérifier qu’elle a bien été transmise par le formulaire avec isset($_POST['formStartTime'])
. Cette étape évite des erreurs si la donnée est absente.
Ensuite, nous calculons le temps écoulé entre le chargement du formulaire et son envoi. La fonction time()
nous donne l’heure actuelle en secondes. La valeur de formStartTime
, quant à elle, est en millisecondes, donc nous devons la diviser par 1000 et la convertir en entier. En soustrayant ces deux valeurs, nous obtenons la durée totale d’interaction avec le formulaire.
Si cette durée est inférieure à trois secondes, nous supposons que la soumission est trop rapide pour être humaine, ce qui est un signe d’automatisation. Nous enregistrons alors cette tentative suspecte avec logAttempt()
, puis nous interrompons immédiatement le script via die("Erreur : Envoi trop rapide.");
.
Ce mécanisme permet d’éliminer les envois quasi instantanés réalisés par des robots et renforce ainsi la sécurité du formulaire.
Limiter le nombre de tentatives
Une autre méthode efficace pour sécuriser un formulaire consiste à limiter le nombre de tentatives de soumission. Un utilisateur légitime remplit généralement un formulaire correctement en une ou deux tentatives, tandis qu’un robot ou un attaquant peut essayer d’envoyer des requêtes en boucle.
Nous allons donc implémenter un compteur de tentatives en stockant ces informations dans la session. Si un utilisateur dépasse un certain nombre d’essais en peu de temps, nous bloquerons temporairement l’accès au formulaire.
Implémentation du compteur de tentatives
Dans send.php
, nous allons ajouter ce contrôle :
session_start();
if (!isset($_SESSION['attempts'])) {
$_SESSION['attempts'] = 0;
$_SESSION['attempt_time'] = time();
}
if ($_SESSION['attempts'] >= 5 && (time() - $_SESSION['attempt_time'] < 300)) {
logAttempt("Trop de tentatives depuis IP: " . $_SERVER['REMOTE_ADDR']);
die("Erreur : Trop de tentatives. Veuillez réessayer plus tard.");
}
$_SESSION['attempts']++;
if ($_SESSION['attempts'] === 1) {
$_SESSION['attempt_time'] = time();
}
- Initialisation des tentatives : Si la variable
$_SESSION['attempts']
n’existe pas encore, nous la créons avec une valeur de0
, ainsi qu’un timestamp$_SESSION['attempt_time']
. - Vérification du nombre d’essais : Si un utilisateur dépasse 5 tentatives en moins de 5 minutes, nous bloquons l’accès au formulaire en affichant un message d’erreur.
- Incrémentation des tentatives : Chaque soumission incrémente le compteur.
- Réinitialisation du délai : Si c’est la première tentative, nous mettons à jour l’horodatage de début.
Ce système empêche les attaques par force brute et limite les abus en imposant un délai raisonnable entre plusieurs essais infructueux.
Conclusion
Grâce à ces différentes protections, notre formulaire est désormais bien mieux armé contre les tentatives d’abus et les soumissions automatisées. Toutefois, pour renforcer encore davantage la sécurité, nous pourrions ajouter une dernière barrière : un CAPTCHA personnalisé. Nous explorerons cette solution dans un prochain article.