Un formulaire PHP est la porte d’entrée principale de ton application : contact, inscription, connexion, commentaire… et donc la cible numéro un des attaques. Injection SQL, XSS, CSRF, spam, envoi de fichiers piégés : voici les 7 protections à mettre en place, avec un exemple complet et prêt à l’emploi.
Un formulaire PHP semble simple : quelques champs HTML, un $_POST, une requête SQL. C’est justement cette simplicité qui le rend dangereux. Toute donnée qui arrive par un formulaire est contrôlée par l’utilisateur, et donc potentiellement par un attaquant. Un seul champ mal traité suffit pour vider une base de données, voler des sessions ou transformer ton site en relais de spam.
La bonne nouvelle, c’est que les protections sont connues, efficaces, et presque toutes disponibles en natif dans PHP. Ce guide les présente une par une, dans l’ordre où une donnée traverse ton application : réception, validation, stockage, affichage. À la fin, tu auras un formulaire PHP de contact complet, sécurisé de bout en bout.

Ne fais jamais confiance aux données d’un formulaire PHP : valide chaque champ côté serveur, utilise des requêtes préparées, échappe à l’affichage avec htmlspecialchars(), protège chaque formulaire par un jeton CSRF, et limite le nombre d’envois.
La règle d’or d’un formulaire PHP sécurisé
Toute la sécurité d’un formulaire PHP repose sur un principe : les données entrantes sont hostiles jusqu’à preuve du contraire. Peu importe que ton formulaire HTML contienne des attributs required, maxlength ou type="email" : ces vérifications ont lieu dans le navigateur, et un attaquant peut les contourner en trois secondes avec les outils de développement ou en envoyant directement une requête.
| Vérification | Où elle a lieu | Rôle |
|---|---|---|
| — | — | — |
required, type="email", pattern | Navigateur | Confort de l’utilisateur, retour immédiat |
| JavaScript de validation | Navigateur | Confort de l’utilisateur |
| Validation PHP | Serveur | Sécurité : la seule qui compte |
La validation côté navigateur améliore l’expérience utilisateur. La validation côté serveur protège ton application. Un formulaire PHP sérieux a toujours les deux, mais ne compte que sur la seconde.
Protection 1 : valider chaque champ côté serveur
Valider, c’est vérifier qu’une donnée correspond exactement à ce que tu attends : un e-mail valide, un nombre dans une plage, un texte d’une longueur raisonnable, une valeur parmi une liste autorisée. Tout ce qui ne correspond pas est rejeté, avec un message clair pour l’utilisateur.
<?php
$erreurs = [];
$nom = trim($_POST['nom'] ?? '');
if ($nom === '' || mb_strlen($nom) > 100) {
$erreurs['nom'] = 'Le nom est obligatoire (100 caractères maximum).';
}
$email = filter_input(INPUT_POST, 'email', FILTER_VALIDATE_EMAIL);
if ($email === false || $email === null) {
$erreurs['email'] = 'Adresse e-mail invalide.';
}
$age = filter_input(INPUT_POST, 'age', FILTER_VALIDATE_INT, [
'options' => ['min_range' => 13, 'max_range' => 120],
]);
if ($age === false || $age === null) {
$erreurs['age'] = 'L’âge doit être un nombre entre 13 et 120.';
}
$sujet = $_POST['sujet'] ?? '';
$sujetsAutorises = ['devis', 'support', 'autre'];
if (!in_array($sujet, $sujetsAutorises, true)) {
$erreurs['sujet'] = 'Sujet invalide.';
}
Quelques points importants dans ce code :
filter_input()lit directement la donnée envoyée et la valide. Il renvoienullsi le champ est absent etfalses’il est invalide.mb_strlen()compte correctement les caractères accentués, contrairement àstrlen()qui compte les octets.- La liste blanche (
in_array(..., true)) est la validation la plus sûre : on n’accepte que des valeurs connues.
Évite de « nettoyer » les données en supprimant des caractères en silence. Si une donnée est invalide, rejette-la et explique pourquoi. Modifier une saisie sans prévenir crée des bugs difficiles à comprendre et masque parfois des tentatives d’attaque.
Protection 2 : bloquer l’injection SQL avec des requêtes préparées
L’injection SQL reste l’une des attaques les plus dévastatrices contre un formulaire PHP. Elle se produit quand une donnée utilisateur est insérée directement dans une requête SQL. L’attaquant peut alors modifier la requête elle-même : contourner une connexion, lire toute la base, voire la supprimer.
<?php
// ❌ VULNÉRABLE : la donnée est collée dans la requête
$sql = "SELECT * FROM utilisateurs WHERE email = '" . $_POST['email'] . "'";
$pdo->query($sql);
// Avec email = ' OR '1'='1 → la condition devient toujours vraie
La solution est la requête préparée : la structure de la requête est envoyée séparément des données, qui ne peuvent donc jamais être interprétées comme du SQL.
<?php
// ✅ SÉCURISÉ : requête préparée avec PDO
$pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', 'user', 'secret', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_EMULATE_PREPARES => false,
]);
$stmt = $pdo->prepare('INSERT INTO messages (nom, email, message) VALUES (:nom, :email, :message)');
$stmt->execute([
'nom' => $nom,
'email' => $email,
'message' => $message,
]);
Retiens une règle simple : aucune variable ne doit jamais apparaître directement dans une chaîne SQL. Même une donnée que tu crois sûre (un identifiant venant de l’URL, une valeur de session) passe par un paramètre. Les seuls éléments qui ne peuvent pas être paramétrés (noms de colonnes, sens de tri) doivent être choisis dans une liste blanche.
Protection 3 : empêcher le XSS en échappant à l’affichage
Le XSS (Cross-Site Scripting) consiste à injecter du code JavaScript dans une page, via un champ de formulaire PHP dont le contenu est ensuite réaffiché : un commentaire, un nom d’utilisateur, un message. Ce script s’exécute alors dans le navigateur des autres visiteurs, et peut voler leurs sessions ou modifier la page.
<?php
// ❌ VULNÉRABLE
echo "<p>Merci " . $_POST['nom'] . " !</p>";
// Avec nom = <script>…</script> → le script s'exécute
// ✅ SÉCURISÉ : échapper au moment de l'affichage
function e(string $valeur): string
{
return htmlspecialchars($valeur, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
echo '<p>Merci ' . e($nom) . ' !</p>';
La bonne pratique consiste à stocker la donnée telle quelle en base, puis à l’échapper au moment de l’affichage, selon le contexte. htmlspecialchars() protège le contenu HTML et les attributs entre guillemets. Pour insérer une donnée dans du JavaScript, utilise plutôt json_encode(), et pour une URL, urlencode().

Protection 4 : le jeton CSRF
Le CSRF (Cross-Site Request Forgery) est plus sournois. Imagine un utilisateur connecté à ton site. Il visite une autre page, piégée, qui contient un formulaire invisible envoyé automatiquement vers ton site. Son navigateur joint son cookie de session : ton application croit que la demande vient de lui, et exécute l’action (changer son e-mail, supprimer un contenu, valider une commande).
La protection classique est le jeton CSRF : une valeur aléatoire, stockée en session et insérée dans chaque formulaire PHP. À la réception, on vérifie que le jeton envoyé correspond à celui de la session. Une page piégée sur un autre site ne peut pas connaître ce jeton.
<?php
// csrf.php
session_start();
function csrf_token(): string
{
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
return $_SESSION['csrf'];
}
function csrf_verifier(): void
{
$envoye = $_POST['csrf'] ?? '';
if (!is_string($envoye) || !hash_equals($_SESSION['csrf'] ?? '', $envoye)) {
http_response_code(403);
exit('Requête refusée.');
}
}
<form method="post" action="/contact.php">
<input type="hidden" name="csrf" value="<?= e(csrf_token()) ?>">
<!-- autres champs -->
</form>
Deux détails comptent : random_bytes() produit une valeur imprévisible (n’utilise jamais rand() ou uniqid() pour la sécurité), et hash_equals() compare à temps constant. Complète cette protection par des cookies de session configurés avec l’attribut SameSite :
<?php
session_set_cookie_params([
'secure' => true, // cookie envoyé uniquement en HTTPS
'httponly' => true, // inaccessible au JavaScript
'samesite' => 'Lax', // non envoyé par la plupart des requêtes venant d'autres sites
]);
session_start();
Protection 5 : limiter le spam et les envois massifs
Un formulaire PHP public, surtout un formulaire de contact, sera tôt ou tard découvert par des robots. Sans protection, tu recevras des centaines de messages publicitaires par jour, et ton serveur d’e-mails pourra être signalé comme source de spam.
- Le champ « pot de miel » (honeypot) : un champ caché en CSS que les humains ne voient pas et ne remplissent pas. Les robots, eux, le remplissent.
- La limitation de fréquence : refuser plus de quelques envois par minute depuis la même session ou la même adresse IP.
- Un délai minimal : un humain met plusieurs secondes à remplir un formulaire, un robot quelques millisecondes.
- Un CAPTCHA en dernier recours, de préférence une solution respectueuse de la vie privée.
<?php
// Pot de miel : le champ "site_web" doit rester vide
if (!empty($_POST['site_web'])) {
exit; // robot détecté, on ne dit rien
}
// Limitation simple : 3 envois maximum par tranche de 10 minutes et par session
$_SESSION['envois'] = array_filter(
$_SESSION['envois'] ?? [],
fn(int $t) => $t > time() - 600
);
if (count($_SESSION['envois']) >= 3) {
http_response_code(429);
exit('Trop de messages envoyés. Réessaie dans quelques minutes.');
}
$_SESSION['envois'][] = time();
Utiliser directement l’adresse e-mail saisie par le visiteur dans les en-têtes From: de la fonction mail(). Un attaquant peut y injecter des retours à la ligne et ajouter des destinataires cachés. Utilise une adresse fixe dans From:, mets celle du visiteur dans Reply-To: après validation, ou mieux, une bibliothèque comme PHPMailer.
Protection 6 : sécuriser l’envoi de fichiers
Un formulaire PHP qui accepte des fichiers (avatar, pièce jointe, CV) est particulièrement exposé. Le danger principal : qu’un attaquant envoie un script PHP déguisé en image, puis l’exécute en l’appelant depuis son navigateur.
<?php
$fichier = $_FILES['avatar'] ?? null;
if (!$fichier || $fichier['error'] !== UPLOAD_ERR_OK) {
exit('Erreur lors de l’envoi.');
}
if ($fichier['size'] > 2 * 1024 * 1024) {
exit('Fichier trop lourd (2 Mo maximum).');
}
// Vérifier le vrai type du fichier, pas l'extension ni le type envoyé par le navigateur
$types = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
$mime = (new finfo(FILEINFO_MIME_TYPE))->file($fichier['tmp_name']);
if (!isset($types[$mime])) {
exit('Format non autorisé.');
}
// Nom généré par le serveur, jamais celui de l'utilisateur
$nom = bin2hex(random_bytes(16)) . '.' . $types[$mime];
move_uploaded_file($fichier['tmp_name'], __DIR__ . '/../uploads/' . $nom);
Les règles à retenir : vérifier le type réel avec finfo, renommer le fichier, limiter la taille, et stocker les fichiers hors du dossier public ou dans un dossier où l’exécution de PHP est désactivée.
Protection 7 : HTTPS et en-têtes de sécurité
Toutes les protections précédentes perdent une partie de leur intérêt si les données de ton formulaire PHP circulent en clair. Sans HTTPS, un mot de passe ou un message peut être lu par n’importe qui sur le même réseau Wi-Fi. Aujourd’hui, la quasi-totalité des hébergeurs proposent un certificat Let’s Encrypt gratuit : il n’y a plus aucune raison de s’en passer.
Ajoute aussi quelques en-têtes de sécurité, qui renforcent la protection contre le XSS et le détournement de clics :
<?php
header('Content-Security-Policy: default-src \'self\'');
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('X-Frame-Options: DENY');

Exemple complet : un formulaire PHP de contact sécurisé
Voici l’assemblage de toutes ces protections dans un formulaire de contact complet, prêt à adapter.
<?php
// contact.php
declare(strict_types=1);
session_set_cookie_params(['secure' => true, 'httponly' => true, 'samesite' => 'Lax']);
session_start();
function e(string $v): string { return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
$erreurs = [];
$ok = false;
$nom = $email = $message = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. Jeton CSRF
if (!hash_equals($_SESSION['csrf'], (string) ($_POST['csrf'] ?? ''))) {
http_response_code(403);
exit('Requête refusée.');
}
// 2. Pot de miel
if (!empty($_POST['site_web'])) {
exit;
}
// 3. Validation
$nom = trim((string) ($_POST['nom'] ?? ''));
$email = trim((string) ($_POST['email'] ?? ''));
$message = trim((string) ($_POST['message'] ?? ''));
if ($nom === '' || mb_strlen($nom) > 100) $erreurs['nom'] = 'Nom obligatoire (100 caractères max).';
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $erreurs['email'] = 'E-mail invalide.';
if (mb_strlen($message) < 10 || mb_strlen($message) > 5000) $erreurs['message'] = 'Message de 10 à 5000 caractères.';
// 4. Enregistrement avec requête préparée
if (!$erreurs) {
$pdo = new PDO('mysql:host=localhost;dbname=app;charset=utf8mb4', 'user', 'secret', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
$pdo->prepare('INSERT INTO messages (nom, email, message) VALUES (?, ?, ?)')
->execute([$nom, $email, $message]);
$ok = true;
$nom = $email = $message = '';
}
}
?>
<form method="post" novalidate>
<input type="hidden" name="csrf" value="<?= e($_SESSION['csrf']) ?>">
<div style="position:absolute;left:-9999px" aria-hidden="true">
<input type="text" name="site_web" tabindex="-1" autocomplete="off">
</div>
<label>Nom <input name="nom" value="<?= e($nom) ?>" required maxlength="100"></label>
<?= isset($erreurs['nom']) ? '<p class="erreur">' . e($erreurs['nom']) . '</p>' : '' ?>
<label>E-mail <input type="email" name="email" value="<?= e($email) ?>" required></label>
<?= isset($erreurs['email']) ? '<p class="erreur">' . e($erreurs['email']) . '</p>' : '' ?>
<label>Message <textarea name="message" required><?= e($message) ?></textarea></label>
<?= isset($erreurs['message']) ? '<p class="erreur">' . e($erreurs['message']) . '</p>' : '' ?>
<button type="submit">Envoyer</button>
<?= $ok ? '<p class="succes">Merci, ton message a bien été envoyé.</p>' : '' ?>
</form>
Ce formulaire PHP réaffiche les valeurs saisies en cas d’erreur (toujours échappées), affiche un message précis par champ, et ne révèle aucune information technique. Pour un formulaire de connexion, ajoute la vérification du mot de passe décrite dans notre guide stocker les mots de passe avec password_hash().
Formulaire PHP : les erreurs à éviter
- Afficher les erreurs PHP en production :
display_errorsdoit être désactivé en ligne, les erreurs vont dans les journaux. Un message d’erreur SQL donne de précieuses indications à un attaquant. - Utiliser
$_REQUEST: il mélange$_GET,$_POSTet parfois les cookies. Lis toujours la source précise que tu attends. - Faire confiance aux champs cachés : un champ
<input type="hidden" name="prix">se modifie aussi facilement qu’un champ visible. Recalcule toujours côté serveur. - Oublier la vérification des droits : un formulaire de modification doit vérifier que l’utilisateur connecté a bien le droit de modifier cet élément.
- Utiliser les fonctions
mysql_*des vieux tutoriels : elles ont été supprimées en PHP 7. Utilise PDO ou MySQLi avec des requêtes préparées.
Avant de mettre un formulaire PHP en ligne, teste-le toi-même en saisissant <script>alert(1)</script> dans chaque champ texte et une apostrophe ' dans chaque champ. Si une alerte s’affiche ou si une erreur SQL apparaît, une protection manque.
FAQ
La validation HTML5 suffit-elle pour sécuriser un formulaire PHP ?
Non. Les attributs required, type="email" ou maxlength sont vérifiés par le navigateur et peuvent être contournés en quelques secondes. Ils améliorent le confort, mais seule la validation côté serveur protège réellement ton application.
Faut-il utiliser htmlspecialchars() avant d’enregistrer en base ?
Non. Stocke la donnée telle qu’elle a été validée, et échappe-la au moment de l’affichage, selon le contexte (HTML, JavaScript, URL). Échapper avant le stockage provoque des doubles encodages et ne protège pas les autres usages de la donnée.
Un jeton CSRF est-il nécessaire sur un formulaire de contact ?
C’est moins critique que sur un formulaire de compte ou de paiement, mais c’est une bonne habitude : il complique aussi l’envoi automatisé par des robots. Sur tout formulaire qui modifie des données d’un utilisateur connecté, il est indispensable.
Les frameworks gèrent-ils ces protections automatiquement ?
Laravel et Symfony intègrent nativement la protection CSRF, l’échappement dans les templates et des requêtes paramétrées via leur ORM. Comprendre ces protections reste essentiel pour ne pas les désactiver par erreur et pour sécuriser un formulaire PHP sans framework.
Un dernier conseil avant le résumé : centralise ces protections. Plutôt que de répéter la vérification du jeton CSRF, la fonction d’échappement et la connexion PDO dans chaque fichier, regroupe-les dans un fichier commun inclus partout. Chaque nouveau formulaire PHP bénéficie alors automatiquement de toutes les protections, et une correction faite à un seul endroit profite à tout le site.
Teste aussi tes formulaires après chaque mise à jour importante : version de PHP, changement d’hébergeur, nouvelle extension. Un comportement par défaut qui change, comme la configuration des cookies de session, peut désactiver une protection sans prévenir.
Et garde une trace de chaque protection dans la documentation de ton projet.
Formulaire PHP : le résumé à retenir
- Toute donnée d’un formulaire PHP est hostile : valide-la côté serveur, champ par champ.
- Utilise des requêtes préparées pour chaque requête SQL, sans exception.
- Échappe chaque donnée à l’affichage avec
htmlspecialchars()et le bon contexte. - Protège chaque formulaire PHP qui modifie des données par un jeton CSRF et des cookies
SameSite. - Limite le spam (pot de miel, fréquence), sécurise les fichiers envoyés et active HTTPS.
Ces sept protections couvrent l’immense majorité des attaques visant un formulaire PHP. Elles demandent quelques dizaines de lignes de code, et évitent des semaines de nettoyage après une intrusion.
Conclusion
Sécuriser un formulaire PHP n’est pas une option réservée aux grands sites : les robots qui cherchent des failles scannent tout Internet, sans distinction de taille. En appliquant ces sept protections dès le départ, tu construis des réflexes qui te serviront dans chaque projet. Pour comprendre les grandes familles de failles derrière ces protections, lis notre guide de l’OWASP Top 10, et si tu développes en local, assure-toi d’utiliser une version récente avec notre guide installer PHP 8.4 et Composer sous Windows.
Sources
- Manuel PHP : htmlspecialchars
- Manuel PHP : requêtes préparées avec PDO
- Manuel PHP : filter_input
- OWASP : Cross-Site Request Forgery Prevention Cheat Sheet
- OWASP : Cross Site Scripting Prevention Cheat Sheet

