Aller au contenu principal
Questions et réponses sur l'hébergement web

Comment un site PHP neutralise les injections passées par ses formulaires

Une injection de code malicieux dans un formulaire est une attaque où la donnée saisie par l'utilisateur sort de son rôle de valeur pour devenir une instruction : requête SQL modifiée, script JavaScript affiché à d'autres visiteurs, commande système. La parade tient dans une règle : toute information reçue par l'application reste une donnée, jamais du code.

Quatre barrières, aucune facultative. Requête préparée pour chaque appel à la base de données, échappement du contenu au moment de l'afficher, jeton secret contre la falsification de requête, validation des données par liste blanche avant tout traitement.

Un réglage serveur prudent (droits SQL réduits, messages d'erreur masqués, en-tête Content-Security-Policy) ne remplace aucune de ces barrières : il réduit les dégâts quand l'une d'elles cède.

Comment une injection de code détourne un formulaire web ?

Une injection de code exploite un mélange que le développeur n'a pas vu : le programme fabrique une instruction (une requête SQL, une page HTML, une commande shell) en collant bout à bout du texte fixe et du texte fourni par le visiteur. Tant que la saisie utilisateur contient ce qu'on attend, un nom ou une adresse e-mail, tout fonctionne. Dès qu'elle contient des caractères spéciaux bien placés (guillemet, chevron, point-virgule), elle casse la structure syntaxique de l'instruction et y ajoute la sienne.

Le serveur ne fait aucune différence entre la partie écrite par le développeur et la partie arrivée par le formulaire web : il reçoit une chaîne complète et l'interprète. C'est ce qui rend l'attaque par injection si répandue. Elle ne demande ni mot de passe volé ni accès au serveur, seulement un champ de saisie dont le contenu atteint un interpréteur sans traitement. L'OWASP, fondation qui publie le classement de référence des risques des applications web, place l'injection dans son Top 10 depuis sa première édition ; la version 2025 la classe en cinquième position, XSS compris.

Trois conditions réunies ouvrent la faille :

  • une donnée contrôlée par l'utilisateur (champ de formulaire, paramètre d'URL, cookie, en-tête HTTP) ;
  • un interpréteur en bout de chaîne (moteur SQL, navigateur, shell, serveur de messagerie) ;
  • une validation insuffisante ou absente entre les deux.

Supprimer l'une des trois suffit à bloquer ce chemin précis. En pratique, on ne supprime jamais la première (un formulaire sert à recevoir des données) ni la deuxième : tout le travail porte sur le passage entre la saisie et l'interpréteur. Les autres réponses de la rubrique CMS et sécurité de R7 traitent des réglages voisins, côté hébergement.

Les types d'injection qui passent par un champ de saisie

Les types d'injection se distinguent par l'interpréteur visé, et donc par la parade. Confondre deux familles conduit à appliquer la mauvaise protection : échapper du HTML avant d'écrire en base ne protège pas la requête SQL, et une requête préparée n'empêche pas un script malveillant de s'afficher plus tard dans une page.

Type d'attaqueInterpréteur viséCe que gagne l'attaquantParade principale
Injection SQLMoteur de base de donnéesLecture, modification ou effacement des tables, contournement d'un loginRequêtes préparées
Cross site scripting (XSS)Navigateur des visiteursVol de session, faux formulaire, redirectionÉchappement selon le contexte, CSP
Injection de commandesShell du serveurPrise de contrôle du compte d'hébergementAucun appel shell avec une saisie, escapeshellarg sinon
Injection d'en-têtes de mailServeur de messagerieEnvoi de spam depuis le domaine du siteRefus des retours chariot, bibliothèque d'envoi
Inclusion et envoi de fichierMoteur PHPExécution d'un code PHP déposé par le visiteurListe fermée de fichiers, stockage hors du dossier web
CSRF (falsification de requête)Le site lui-même, via un visiteur connectéAction faite au nom de la victimeJeton secret lié à la session

Le CSRF n'injecte rien au sens strict : il pousse le navigateur d'un utilisateur authentifié à soumettre un formulaire qu'il n'a pas rempli. Il figure dans ce tableau parce qu'il se traite au même endroit du code, à la réception du formulaire, et que les protections se posent ensemble.

Injection SQL : du champ login à la base de données entière

L'injection SQL reste l'attaque par injection la plus documentée parce qu'un formulaire de connexion en fournit l'exemple parfait. Le développeur veut vérifier qu'un couple identifiant et mot de passe existe ; il assemble la requête SQL avec les deux champs reçus. Le moteur de base de données reçoit alors une instruction dont une partie vient directement du visiteur.

L'exemple du champ login concaténé

Voici le code à ne jamais écrire, encore présent dans de vieux tutoriels et dans des scripts hérités :

$sql = "SELECT id FROM users WHERE login = '" . $_POST['login'] . "' AND pass = '" . $_POST['pass'] . "'";
$result = mysql_query($sql);

Si le visiteur tape admin' -- dans le champ login, la requête devient SELECT id FROM users WHERE login = 'admin' -- ' AND pass = '...'. Les deux tirets ouvrent un commentaire SQL : la vérification du mot de passe disparaît et l'attaquant entre sous le nom d'utilisateur « admin ». La variante ' OR '1'='1 rend la condition toujours vraie et renvoie le premier compte de la table, souvent celui de l'administrateur.

La fonction mysql_query de cet exemple a disparu avec PHP 7 ; un site qui l'appelle encore tourne sur une version de PHP sans correctifs de sécurité depuis des années, ce qui pose un second problème. Avant de migrer, le test de compatibilité d'un vieux site avec PHP 7 et 8 évite les mauvaises surprises.

UNION SELECT, messages d'erreur et injection à l'aveugle

Le contournement d'un login n'est que la porte d'entrée. Une injection union based ajoute à la requête d'origine une seconde requête par UNION SELECT, pour faire afficher dans la page des colonnes d'une autre table : ' UNION SELECT login, pass FROM users -- dans un champ de recherche peut lister tous les comptes si le nombre de colonnes concorde. L'attaquant le trouve par tâtonnement, en ajoutant des colonnes une à une.

Quand la page n'affiche rien des résultats, deux autres techniques prennent le relais. L'injection par message d'erreur exploite un site qui affiche les erreurs SQL brutes : le texte de l'erreur révèle le nom des tables, la version du moteur, parfois des valeurs. L'injection à l'aveugle pose des questions dont la réponse se lit au comportement de la page (elle change ou non) ou au temps de réponse (une fonction SLEEP qui ralentit la requête quand la condition est vraie). Des outils open source comme sqlmap automatisent ces trois techniques : un formulaire vulnérable se vide en quelques minutes, sans compétence particulière.

La parade : requête préparée avec PDO ou mysqli

Une requête préparée envoie au serveur SQL le texte de la requête avec des marqueurs, puis les valeurs à part. Le moteur analyse la requête avant de connaître les valeurs : aucune valeur ne peut plus en modifier la structure. Le manuel PHP l'indique sans détour, une application qui n'utilise que des requêtes préparées se met à l'abri de l'injection SQL.

$pdo = new PDO($dsn, $userBdd, $passBdd, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_EMULATE_PREPARES => false,
]);
$req = $pdo->prepare('SELECT id, pass_hash FROM users WHERE login = ?');
$req->execute([$_POST['login']]);
$compte = $req->fetch();
if ($compte && password_verify($_POST['pass'], $compte['pass_hash'])) {
    // connexion acceptée
}

Cet exemple corrige deux défauts d'un coup : la requête SQL ne contient plus aucune donnée du visiteur, et le mot de passe n'est plus comparé en clair mais vérifié contre une empreinte produite par password_hash. L'option ATTR_EMULATE_PREPARES à false demande au pilote MySQL une vraie préparation côté serveur plutôt qu'une émulation par PHP.

Les marqueurs remplacent des valeurs entières, jamais un nom de table, un nom de colonne ou un sens de tri. Pour un tri choisi par le visiteur (ORDER BY prix ou ORDER BY date), le code compare la valeur reçue à une liste fermée de colonnes autorisées et prend une valeur par défaut sinon. Logique identique pour LIMIT : on convertit en entier avec (int) ou on passe le nombre en paramètre typé.

Cross site scripting (XSS) : le formulaire qui sert un script malveillant

Le cross site scripting (XSS) vise les visiteurs plutôt que le serveur. L'attaquant dépose dans un champ de formulaire du code HTML ou du code JavaScript ; si le site renvoie ce contenu dans une page sans l'échapper, le navigateur de chaque utilisateur l'interprète comme une partie légitime du site. Le script tourne alors avec les droits du domaine : il lit les cookies non protégés, modifie la page, affiche un faux formulaire de connexion ou envoie des requêtes au nom de la victime.

Reflected, stored, DOM : trois façons de servir le script

Reflected XSS
La saisie revient aussitôt dans la réponse, par exemple un moteur de recherche interne qui affiche « Aucun résultat pour » suivi du terme cherché. L'attaquant fabrique un lien piégé et le fait cliquer à sa victime.
Stored XSS
Le contenu s'enregistre en base (commentaire, profil, avis, message de contact lu dans un back-office) et s'affiche à chaque consultation. C'est l'attaque XSS la plus grave : une seule saisie touche tous les lecteurs, administrateurs compris.
DOM XSS
Le code JavaScript du site lui-même lit une valeur (fragment d'URL, champ) et l'écrit dans la page par innerHTML ou document.write. Le serveur ne voit jamais passer la charge, d'où des filtres côté serveur inopérants.

Échapper selon le contexte d'affichage

La règle tient en un geste : on stocke la donnée telle qu'elle arrive, on l'échappe au moment de l'afficher, avec la fonction adaptée au contexte. Dans le corps d'une page HTML ou dans un attribut entre guillemets, htmlspecialchars convertit les caractères spéciaux en entités HTML :

function e(string $v): string {
    return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
echo '<p>' . e($commentaire) . '</p>';
echo '<input name="ville" value="' . e($ville) . '">';

Les autres contextes demandent d'autres outils. Pour passer une valeur à un bloc de code JavaScript, json_encode avec les options JSON_HEX_TAG et JSON_HEX_AMP produit un littéral sûr. Pour une adresse de site fournie par un membre, il faut vérifier que l'URL commence par https:// ou http://, sinon un lien javascript: déclenche du code au clic. Un moteur de gabarits comme Twig, ou le moteur Blade de Laravel, échappe par défaut chaque variable affichée ; le risque revient dès qu'on désactive cet échappement avec un filtre raw.

Quand le site doit accepter du HTML (un éditeur de texte riche), l'échappement ne convient plus : une bibliothèque de nettoyage par liste blanche, HTML Purifier en PHP, ne garde que les balises et attributs permis. Un filtre maison à base d'expressions régulières finit toujours contourné.

Content-Security-Policy, seconde barrière

L'en-tête HTTP Content-Security-Policy indique au navigateur d'où le site accepte de charger du code. Une politique script-src 'self' interdit les scripts en ligne et ceux d'un domaine tiers : un script malveillant injecté malgré tout ne tourne pas. La documentation MDN de Mozilla détaille les directives. La mise en place demande de sortir le JavaScript des attributs onclick et des balises en ligne, travail réel sur un site ancien, mais elle transforme une faille XSS exploitable en simple défaut d'affichage. Le drapeau HttpOnly sur le cookie de session complète le dispositif : le code JavaScript ne peut plus le lire.

Commandes système et fichiers envoyés : l'injection qui vise le serveur

L'injection de commandes touche les scripts qui appellent le système d'exploitation : conversion d'image avec exec('convert ...'), ping d'un nom d'hôte saisi, archivage. Si la saisie entre dans la commande sans traitement, un point-virgule ou une barre verticale suffit pour enchaîner une seconde commande, qui s'exécute avec les droits du compte web. Sur un hébergement mutualisé, cela donne accès à tous les fichiers du compte, configuration de la base comprise.

La meilleure protection consiste à ne pas appeler le shell : PHP dispose d'extensions natives pour la plupart des besoins (GD ou Imagick pour les images, ZipArchive pour les archives). Quand l'appel reste inévitable, chaque argument passe par escapeshellarg, et la valeur est d'abord comparée à un format strict. Beaucoup d'hébergeurs coupent d'office exec, system ou shell_exec ; les raisons de ces fonctions que l'hébergeur désactive dans PHP tiennent précisément à ce risque.

Le champ d'envoi de fichier ouvre une autre voie pour injecter du code. Un utilisateur malveillant qui dépose avatar.php dans un dossier public, puis appelle son adresse, fait tourner son propre programme sur le serveur. Les protections se cumulent :

  • vérifier le type réel du fichier avec finfo, pas l'extension ni le type annoncé par le navigateur ;
  • renommer le fichier avec un identifiant aléatoire et une extension choisie par le code ;
  • le ranger hors du dossier web, ou dans un dossier où le serveur refuse d'interpréter PHP ;
  • pour une image, la recréer avec GD : l'opération supprime le code caché dans les métadonnées.

Précaution identique pour les inclusions : un paramètre ?page=contact qui alimente un include se compare à une liste fermée de pages, sans quoi un chemin du type ../../config ouvre des fichiers sensibles.

Injection d'en-têtes dans un formulaire de contact

Le formulaire de contact paraît sans enjeu : il envoie un mail au propriétaire du site. Pourtant, quand l'adresse saisie par le visiteur sert d'en-tête From ou Reply-To, un retour chariot suivi de Bcc: liste@exemple.org ajoute des destinataires cachés. Le formulaire devient un relais de spam qui part du domaine du site, et la réputation du domaine s'effondre : les messages légitimes finissent en courrier indésirable, voire refusés, comme dans le cas d'un mail envoyé par script PHP que Gmail rejette.

La correction combine trois gestes. D'abord, refuser toute valeur qui contient \r ou \n avant de l'utiliser dans un en-tête. Ensuite, valider l'adresse avec filter_var($email, FILTER_VALIDATE_EMAIL). Enfin, préférer une bibliothèque d'envoi maintenue, PHPMailer ou Symfony Mailer, qui vérifie les en-têtes d'elle-même et passe par un serveur SMTP authentifié. L'adresse de l'expéditeur réel reste celle du domaine ; celle du visiteur va dans Reply-To après validation.

CSRF : un formulaire envoyé à l'insu de l'utilisateur

Une attaque CSRF repose sur un comportement normal du navigateur : il joint les cookies du site à toute requête vers ce site, même lancée depuis une page tierce. Un administrateur connecté qui visite une page piégée peut ainsi, sans le savoir, envoyer le formulaire « changer l'adresse e-mail du compte » ou « créer un utilisateur » du back-office.

Le mécanisme de protection standard est un jeton secret, différent pour chaque session, inséré dans chaque formulaire et vérifié à la réception :

// à l'ouverture de session
if (empty($_SESSION['csrf'])) {
    $_SESSION['csrf'] = bin2hex(random_bytes(32));
}
// dans le formulaire
echo '<input type="hidden" name="csrf" value="' . e($_SESSION['csrf']) . '">';
// à la réception
if (!hash_equals($_SESSION['csrf'], $_POST['csrf'] ?? '')) {
    http_response_code(403);
    exit;
}

Le site tiers ne connaît pas la valeur du jeton et ne peut pas la lire. La fonction hash_equals compare en temps constant, ce qui empêche de deviner le jeton signe par signe. L'attribut SameSite=Lax sur le cookie de session, réglage par défaut de Chrome depuis 2020, ajoute une protection, sans dispenser du jeton pour les requêtes POST sensibles.

Valider les données : liste blanche plutôt que nettoyage

La validation des données ne remplace pas les requêtes préparées ni l'échappement, mais elle réduit la surface d'attaque et attrape les erreurs de bonne foi. Le bon réflexe consiste à décrire ce qu'une valeur a le droit de contenir, puis à refuser tout le reste, plutôt qu'à chercher des motifs dangereux à retirer. Un nettoyage qui supprime le mot script se contourne avec scrscriptipt ; une règle « un code postal compte cinq chiffres » ne se contourne pas.

  • Entier : filter_var($v, FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]).
  • Adresse e-mail : FILTER_VALIDATE_EMAIL, plus une longueur maximale.
  • Choix dans une liste (pays, catégorie, tri) : in_array($v, $valeursPermises, true).
  • Texte libre : longueur maximale, encodage UTF-8 valide vérifié par mb_check_encoding, aucune autre contrainte, puisque l'échappement s'en charge à l'affichage.
  • Date : DateTime::createFromFormat avec le format attendu, puis contrôle que la conversion n'a rien corrigé.

La validation se fait toujours côté serveur. Les attributs HTML required, maxlength ou pattern et les contrôles JavaScript améliorent le confort du visiteur, mais un attaquant envoie ses requêtes avec un outil comme curl et ne passe jamais par le navigateur. Une valeur hors règle se refuse avec un message clair ; elle ne se « répare » pas en silence, car une donnée modifiée à l'insu de l'utilisateur crée d'autres bogues.

Réglages du serveur qui réduisent la surface d'attaque

Le code du site reste la première ligne de défense, mais quelques réglages côté hébergement limitent les dégâts d'une faille oubliée. Ce sont des mesures de sécurité de second rang, à poser une fois pour toutes.

  • Privilèges SQL minimaux : le compte du SGBD utilisé par le site public ne reçoit que les droits nécessaires, SELECT, INSERT, UPDATE et DELETE sur ses propres tables. Sans DROP, GRANT ni FILE, une injection SQL réussie ne peut ni détruire les tables ni écrire un fichier sur le disque.
  • Erreurs masquées : display_errors = Off en production et un journal d'erreurs actif. Un message d'erreur SQL affiché au visiteur guide l'attaquant ; dans le journal, il guide le développeur.
  • Version de PHP suivie : une branche encore maintenue reçoit les correctifs. Le site php.net publie le calendrier de fin de support de chaque branche.
  • Pare-feu applicatif : ModSecurity avec les règles OWASP CRS, ou le filtrage proposé par un CDN, bloque les charges les plus banales. Il arrête les robots, pas un attaquant patient, et ne dispense d'aucune correction dans le code.
  • Limite de fréquence : un nombre maximal d'envois par adresse IP et par minute freine les outils automatiques qui testent des centaines de variantes.

Formulaire par formulaire : ce qu'il faut vérifier sur un site PHP

Un audit rapide d'un site existant passe en revue chaque formulaire avec la question : où va cette donnée, et quel interpréteur la lit ? La grille suivante couvre les formulaires les plus courants d'un site vitrine ou d'un petit site communautaire.

Formulaire de connexion
Requête préparée sur le login, password_verify sur une empreinte, nouveau numéro de session par session_regenerate_id(true) après connexion, délai croissant après plusieurs échecs, message identique que le compte existe ou non.
Moteur de recherche interne
Requête préparée, y compris pour un LIKE (les signes % et _ de la saisie s'échappent), terme cherché échappé à l'affichage du titre « Résultats pour ». C'est le terrain favori du reflected XSS.
Formulaire de contact
Adresse validée, retours chariot refusés dans les en-têtes, bibliothèque d'envoi, champ piège invisible contre les robots, contenu du message échappé s'il s'affiche dans un back-office.
Commentaires et avis
Stockage brut par requête préparée, échappement systématique à l'affichage, modération avant publication, CSP sur les pages qui les affichent.
Envoi de fichier
Type réel vérifié, taille plafonnée, nom aléatoire, stockage hors du dossier interprété par PHP.
Back-office
Jeton CSRF sur chaque action, authentification forte, et la même rigueur que sur les pages publiques : un administrateur peut cliquer un lien piégé comme n'importe qui.

Comment détecter une injection de code sur un site ?

Détecter une injection de code se fait à deux moments du cycle de vie d'un site : avant la mise en ligne, en lisant le code source, et en production, en surveillant ce qui arrive au serveur. Les deux approches se complètent.

Avant la mise en ligne : analyse statique et tests

L'analyse statique lit le code sans l'exécuter et suit le trajet de chaque donnée reçue ($_GET, $_POST, $_COOKIE) jusqu'aux fonctions sensibles. Pour PHP, Psalm propose une analyse de flux de données (« taint analysis ») qui signale une variable du visiteur arrivée dans une requête SQL ou un echo sans traitement ; Semgrep, outil open source multilangage, applique des règles du même ordre. Une recherche manuelle donne déjà beaucoup : toute concaténation dans un appel à query(, tout echo $_, tout exec( mérite une relecture.

Les tests dynamiques attaquent l'application comme le ferait un intrus. OWASP ZAP, scanner open source, envoie des charges connues dans chaque champ et observe les réponses ; sqlmap se concentre sur l'injection SQL. Ces outils ne se lancent que sur un site dont on a la responsabilité, idéalement une copie de test : sur le site d'un tiers, la même manœuvre constitue une intrusion punie par le Code pénal.

En production : journaux et signes d'alerte

Les journaux d'accès du serveur gardent la trace des tentatives. Des motifs reviennent : UNION SELECT, ' OR 1=1, <script, ../, SLEEP(, souvent encodés en URL. Une rafale de requêtes vers le même formulaire depuis une seule adresse IP signale un outil automatique. Côté base, une hausse des erreurs SQL dans le journal mérite une enquête, de même qu'un compte administrateur créé sans raison, un fichier PHP inconnu dans un dossier d'images ou une page qui redirige soudain vers un autre site.

Si une injection a réussi, la remise en état suit un ordre fixe : corriger la faille, changer tous les mots de passe (base, comptes, FTP), invalider les sessions, comparer les fichiers à une copie saine, restaurer la base si des données ont changé. Le RGPD impose en outre de notifier la CNIL sous 72 heures quand la violation touche des données personnelles et présente un risque pour les personnes concernées.

Formulaires fournis par un CMS ou une extension

Un site sous WordPress, Joomla ou PrestaShop n'écrit pas lui-même ses requêtes SQL : il hérite des protections du cœur et de celles des extensions. Le cœur de ces CMS utilise des requêtes préparées ($wpdb->prepare chez WordPress) et des fonctions d'échappement (esc_html, esc_attr) ; la plupart des failles publiées touchent les extensions, surtout celles qui ne reçoivent plus de mises à jour. Les bases de vulnérabilités publiques, comme celle de Wordfence ou de Patchstack pour WordPress, recensent chaque semaine des extensions de formulaires, de sliders ou de boutiques vulnérables à l'injection SQL ou au XSS.

Trois habitudes réduisent le risque : installer peu d'extensions, choisir celles qu'un éditeur actif maintient, appliquer les mises à jour vite. Pour un premier site, la question de choisir un CMS quand on débute rejoint celle de la sécurité : un outil répandu et suivi publie ses correctifs plus vite. Le code ajouté à la main dans un thème enfant ou un extrait PHP suit les règles de cette page, sans exception : $wpdb->query avec une variable concaténée reste une injection SQL, CMS ou pas.

Addslashes, filtres, captcha : ce qui protège vraiment un formulaire

La fonction addslashes suffit-elle contre l'injection SQL ?

Non, la fonction addslashes ne protège pas de façon fiable : elle ignore le jeu de signes de la connexion et laisse passer les valeurs numériques non entourées de guillemets. Les magic quotes, qui l'appliquaient d'office, ont disparu en PHP 5.4. Seules les requêtes préparées offrent une protection fiable.

Un captcha bloque-t-il les injections de code ?

Non, un captcha freine les robots qui remplissent les formulaires en masse, mais un attaquant humain le résout puis envoie sa charge. Il réduit le volume de tentatives automatiques sans corriger la faille, qui se traite dans le code.

Faut-il échapper les données avant de les enregistrer en base ?

Non, la donnée s'enregistre telle qu'elle arrive, par une requête préparée, et s'échappe au moment de l'affichage selon le contexte. Échapper avant l'enregistrement abîme les données et protège mal, parce qu'une même valeur s'affiche ensuite en HTML, en JSON ou dans un mail.

Le HTTPS protège-t-il un formulaire contre les injections ?

Non, le HTTPS chiffre le trajet entre le navigateur et le serveur : il empêche l'écoute et la modification en route, pas l'envoi volontaire d'une charge par l'attaquant lui-même. Les deux protections se cumulent.

Sources consultées en octobre 2026 : manuel PHP (requêtes préparées PDO, htmlspecialchars, random_bytes, hash_equals, filter_var, password_hash), documentation MDN de Mozilla sur Content-Security-Policy et SameSite, OWASP Top 10 2025 et fiches de l'OWASP sur l'injection SQL, le XSS et le CSRF, article 33 du RGPD sur la notification des violations.

Dernières réponses

Questions proches

▲