Comment prévenir les injections : requêtes préparées et validation côté serveur
Un bon auditeur ne se contente pas de trouver des failles : il sait expliquer comment les corriger. Les injections ont cette particularité : leur remède est connu, efficace et relativement simple. La difficulté est de l’appliquer partout, systématiquement. Cette leçon présente les défenses essentielles, avec des exemples de code que vous pourrez comparer à ceux que vous avez vus dans les leçons précédentes.
Le principe : séparer le code et les données
La cause des injections est le mélange. Le remède est donc la séparation : on envoie à l’interpréteur, d’un côté, la structure de l’instruction, de l’autre, les valeurs. L’interpréteur ne confondra plus jamais une donnée avec un ordre, quels que soient les caractères qu’elle contient. C’est comme remplir un formulaire administratif : les rubriques sont imprimées (le « programme »), et vous écrivez seulement dans les cases prévues (les « données »). Même si vous écrivez une instruction dans une case, elle reste du texte de case.
Défense n° 1 : les requêtes préparées (paramétrées)
Pour SQL, la mesure principale est la requête préparée. On écrit la requête avec des emplacements pour les valeurs, puis on les fournit séparément. Comparez :
// À éviter : concaténation
$requete = "SELECT nom, email FROM utilisateurs WHERE id = '" . $id . "'";
// À préférer : requête préparée avec un paramètre nommé
$stmt = $pdo->prepare("SELECT nom, email FROM utilisateurs WHERE id = :id");
$stmt->execute([':id' => $id]);
$lignes = $stmt->fetchAll();
La base reçoit d’abord la forme de la requête, qu’elle analyse une fois pour toutes, puis les valeurs. Une apostrophe dans $id n’est plus qu’un caractère d’une valeur. La même idée existe dans tous les langages et toutes les bibliothèques d’accès aux bases de données, sous le nom de requêtes paramétrées :
# Python, avec une bibliothèque d'accès aux bases de données
curseur.execute("SELECT nom, email FROM utilisateurs WHERE id = %s", (identifiant,))
Attention à un piège : une requête « préparée » dans laquelle on a quand même concaténé une partie variable n’est pas protégée. La règle : aucune donnée externe dans le texte de la requête, seulement dans les paramètres.
Cas particuliers : ce qu’on ne peut pas paramétrer
Certaines parties d’une requête ne peuvent pas être des paramètres : un nom de colonne, un nom de table, un sens de tri. Si l’utilisateur peut choisir ces éléments (« trier par prix »), on utilise une liste blanche : le programme associe chaque choix autorisé à une valeur connue et refuse tout le reste.
$colonnesAutorisees = ['nom', 'prix', 'date'];
$tri = in_array($_GET['tri'], $colonnesAutorisees, true) ? $_GET['tri'] : 'nom';
$requete = "SELECT nom, prix FROM produits ORDER BY " . $tri;
Ici, la valeur finalement insérée provient du programme (la liste), jamais directement de l’utilisateur.
Défense n° 2 : valider les entrées côté serveur
La validation est un complément, pas un substitut. Elle réduit la surface d’attaque et attrape les erreurs de bonne foi. Quelques principes :
- Valider côté serveur, toujours. La validation côté navigateur est un confort d’usage ; un testeur ou un attaquant l’ignore en envoyant directement ses requêtes.
- Préférer la liste blanche à la liste noire : définir ce qui est accepté (format, longueur, jeu de caractères) plutôt que tenter de lister ce qui est interdit.
- Vérifier le type : un identifiant numérique est converti en entier ; ce qui n’en est pas un est rejeté.
- Normaliser avant de valider (encodage, espaces), pour éviter les contournements.
- Rejeter plutôt que « nettoyer » quand c’est possible : supprimer des caractères dangereux est fragile.
$id = filter_var($_GET['id'] ?? '', FILTER_VALIDATE_INT);
if ($id === false || $id < 1) {
http_response_code(400);
exit('Requête invalide');
}
Défense n° 3 : limiter les dégâts potentiels
Même bien programmé, un système peut contenir une erreur. On applique donc la défense en profondeur :
- Droits minimaux : le compte de base de données utilisé par l’application ne doit avoir que les permissions nécessaires (en général, lecture et écriture sur ses propres tables, sans droit d’administration). Une injection sur un compte limité cause bien moins de dégâts.
- Séparation des comptes : un compte en lecture seule pour les pages qui ne font que lire.
- Messages d’erreur génériques pour l’utilisateur, détails techniques dans les journaux internes uniquement.
- Surveillance : journaliser les requêtes anormales et les erreurs de base de données, et alerter en cas de pic.
- Pare-feu applicatif (WAF) : un filet de sécurité qui bloque les motifs d’attaque connus, utile en complément mais jamais en remplacement de la correction du code.
Défense pour l’exécution de commandes et les autres interpréteurs
| Interpréteur | Meilleure défense |
|---|---|
| SQL | Requêtes préparées ; liste blanche pour noms de colonnes et tris ; droits minimaux |
| Commande système | Éviter d’appeler le shell ; utiliser les bibliothèques du langage ; sinon, appeler le programme avec une liste d’arguments séparés et valider les valeurs par liste blanche |
| LDAP | Échapper correctement les valeurs dans les filtres avec la fonction dédiée de la bibliothèque, valider le format |
| NoSQL | Vérifier le type des entrées (une chaîne attendue, pas un objet), utiliser les interfaces de requête paramétrées |
| Modèles de pages, XML | Ne jamais traiter de saisie comme un modèle ; désactiver la résolution d’entités externes |
Notez la première ligne de la défense « commande système » : la meilleure protection consiste souvent à ne pas appeler du tout un shell. Beaucoup de tâches (créer une archive, tester une connexion, redimensionner une image) disposent de bibliothèques dédiées.
Les mauvaises idées qui subsistent
- « J’échappe les apostrophes avec une fonction maison » : fragile, selon le contexte et l’encodage.
- « Je supprime les mots dangereux » : se contourne presque toujours.
- « L’ORM s’occupe de tout » : un outil de mapping objet-relationnel protège quand on utilise ses fonctions paramétrées, mais pas quand on lui fournit du SQL brut concaténé.
- « C’est une application interne » : les attaques viennent aussi de l’intérieur, ou d’un poste compromis.
Documenter la correction dans le rapport
Dans votre carnet, la rubrique « correction suggérée » doit être concrète, actionnable et proportionnée. Par exemple, pour la faille du module de DVWA : « Remplacer la construction de la requête par une requête préparée avec un paramètre nommé ; convertir l’identifiant en entier côté serveur ; utiliser un compte de base de données en lecture seule pour cette page ; afficher un message d’erreur générique. » Le développeur peut agir immédiatement. Pour approfondir, les aide-mémoire de l’OWASP sur la prévention des injections sont une référence gratuite (owasp.org).
Astuce
Après correction, retestez. Un bon rapport de suivi mentionne « constat corrigé et vérifié le… » : c’est la preuve que la boucle est bouclée.
À vous de jouer
Corrigez ce code (exercice d’écriture, rien à attaquer) :
$categorie = $_GET['cat'];
$ordre = $_GET['ordre'];
$requete = "SELECT titre, prix FROM produits WHERE categorie = '" . $categorie . "' ORDER BY " . $ordre;
$resultat = $pdo->query($requete);
- Identifiez les deux entrées non fiables.
- Réécrivez la requête avec un paramètre préparé pour la catégorie et une liste blanche pour l’ordre de tri.
- Ajoutez deux mesures de défense en profondeur.
Voir une solution
$tris = ['titre', 'prix'];
$ordre = in_array($_GET['ordre'] ?? '', $tris, true) ? $_GET['ordre'] : 'titre';
$stmt = $pdo->prepare("SELECT titre, prix FROM produits WHERE categorie = :cat ORDER BY " . $ordre);
$stmt->execute([':cat' => $_GET['cat'] ?? '']);
$resultat = $stmt->fetchAll();
La catégorie devient un paramètre ; l’ordre de tri, qui ne peut pas être un paramètre, vient d’une liste blanche. Mesures complémentaires : compte de base de données en lecture seule pour cette page, et message d’erreur générique avec journalisation détaillée côté serveur.
À retenir
- Le remède racine : séparer instructions et données, via des requêtes préparées pour SQL.
- Ce qui ne peut pas être un paramètre (colonne, tri) passe par une liste blanche.
- La validation côté serveur, de préférence par liste blanche, est un complément indispensable.
- Défense en profondeur : droits minimaux, erreurs génériques, journalisation, pare-feu applicatif en filet.
- Pour les commandes système, la meilleure défense est de ne pas appeler le shell.
- Après correction, retestez.
Vous avez fait le tour des injections côté serveur. Le chapitre suivant se déplace chez l’utilisateur : les failles qui s’exécutent dans le navigateur.