Injection SQL et requêtes préparées
Sommaire
OWASP Top 10 — A03:2021 Injection. La vulnérabilité applicative la plus classique, et la plus facile à corriger correctement.
1. Le code vulnérable
<?php
$login = $_POST['login'] ?? '';
$password = $_POST['password'] ?? '';
$sql = "SELECT * FROM joueurs WHERE pseudo = '$login' AND mdp = '$password'";
$result = mysqli_query($conn, $sql);
Le défaut tient en une phrase : la saisie utilisateur est concaténée dans la requête. Le moteur SQL reçoit une chaîne unique et ne peut plus distinguer ce qui est du code de ce qui est de la donnée.
2. Exploitation
Dans le champ login, saisir :
admin_gameon' OR '1'='1' -- e
La requête réellement exécutée devient :
SELECT * FROM joueurs WHERE pseudo = 'admin_gameon' OR '1'='1' -- e' AND mdp = '...'
Décomposition :
- La première apostrophe ferme prématurément la chaîne
pseudo = '...'. OR '1'='1'ajoute une condition toujours vraie. CommeORsuffit qu'un seul membre soit vrai, la clauseWHEREest satisfaite pour toutes les lignes.--ouvre un commentaire SQL : tout ce qui suit, dont la vérification du mot de passe, est purement ignoré. (L'espace après--est requis par MySQL, d'où le caractère final.)
La requête retourne donc au moins une ligne, mysqli_num_rows() > 0 est vrai, et le code
ouvre la session avec le premier compte trouvé — souvent l'administrateur. L'authentification n'est
pas cassée : elle est contournée, le mot de passe n'a jamais été évalué.
Défaut aggravant dans ce code : le message d'erreur affiche le login saisi (« $login introuvable »). Il renseigne l'attaquant sur l'existence des comptes et peut renvoyer des fragments de message d'erreur SQL. Un message générique, identique dans tous les cas d'échec, est la règle.
3. Automatisation avec sqlmap
sqlmap -u "http://IP_WEB/login.php" --data="login=test&password=test" --dbs
sqlmap -u "http://IP_WEB/login.php" --data="login=test&password=test" -D gameon --tables
sqlmap -u "http://IP_WEB/login.php" --data="login=test&password=test" -D gameon -T joueurs --dump
L'outil détecte le point d'injection, énumère bases et tables, puis extrait le contenu. Il démontre que le problème dépasse le contournement du login : c'est la base entière qui est lisible.
Usage strictement limité à une infrastructure dont on a la responsabilité, ou à un périmètre d'audit autorisé par écrit.
4. Correction : requêtes préparées
<?php
session_start();
require 'db.php';
$erreur = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$login = $_POST['login'] ?? '';
$password = $_POST['password'] ?? '';
$sql = "SELECT id, pseudo, mdp, role FROM joueurs WHERE pseudo = :pseudo";
$stmt = $pdo->prepare($sql);
$stmt->execute([':pseudo' => $login]);
$user = $stmt->fetch(PDO::FETCH_ASSOC);
if ($user && password_verify($password, $user['mdp'])) {
session_regenerate_id(true);
$_SESSION['user'] = $user['pseudo'];
$_SESSION['user_id'] = $user['id'];
$_SESSION['role'] = $user['role'];
header('Location: profile.php');
exit;
}
$erreur = "Identifiants incorrects.";
}
Pourquoi la requête préparée empêche l'injection
Le mécanisme se déroule en deux temps distincts :
prepare()envoie au SGBD la structure de la requête, avec des emplacements (:pseudo). Le moteur analyse et compile ce plan d'exécution avant de connaître la valeur.execute()transmet les valeurs séparément. Elles sont traitées comme des données typées, jamais réinterprétées comme du SQL.
Conséquence : saisir ' OR '1'='1' -- revient à chercher un joueur dont le pseudo est littéralement
cette chaîne. Aucun résultat, aucune injection. Ce n'est pas un filtrage de caractères — c'est une
séparation structurelle entre code et données, qu'aucun échappement astucieux ne peut contourner.
password_verify() plutôt qu'une comparaison
Le mot de passe est stocké haché, jamais en clair. password_verify() rehache la
saisie avec le sel et le coût extraits du hash stocké, puis compare.
- Un simple
==supposerait un stockage en clair, ce qui est la faute d'origine. password_verify()effectue une comparaison à temps constant, qui ne fuit pas d'information par la durée de traitement (attaque temporelle).- Comparer soi-même deux hachages avec
==expose en PHP au piège du transtypage de chaînes numériques (magic hashes).
5. Régénérer la session
session_regenerate_id(true) attribue un nouvel identifiant de session juste après
l'authentification, et détruit l'ancien.
Cela contre la fixation de session : un attaquant qui aurait imposé à la victime un identifiant de session connu de lui (via un lien piégé) le verrait devenir valide au moment de la connexion. En régénérant, l'identifiant qu'il détient devient inutilisable.
6. Conséquences pour l'organisation
- Financières : coût de l'investigation, de la remédiation, de l'interruption de service.
- Réglementaires : violation de données personnelles, notification à la CNIL sous 72 h (art. 33 RGPD), sanction possible jusqu'à 20 M€ ou 4 % du CA mondial.
- Judiciaires : actions en réparation des personnes concernées, éventuelle action de groupe.
- Image : perte de confiance des utilisateurs, effet durable sur l'acquisition.
- En cascade : les mots de passe extraits sont réutilisés ailleurs (credential stuffing) — l'impact déborde largement l'organisation victime.