Injection SQL
- Comprendre comment une application vulnérable peut être attaquée via une injection SQL.
- Comprendre que hacher un mot de passe ne suffit pas à empêcher une injection SQL.
- Apprendre à sécuriser une requête SQL en utilisant PDO avec une requête préparée
(
prepare()+bindParam()).
Si vos requêtes SQL ne sont pas correctement sécurisées, un attaquant peut injecter du code SQL malveillant pour accéder, modifier voire supprimer votre base de données.
1. Création de la base de données
Créez une base de données site_marchand et une table comptes, qui simule les comptes clients
d'un site marchand fictif (identifiant, mot de passe, et numéro de carte bancaire associé).
CREATE DATABASE site_marchand;
USE site_marchand;
CREATE TABLE comptes (
id INT PRIMARY KEY AUTO_INCREMENT,
identifiant VARCHAR(30) NOT NULL UNIQUE,
motdepasse VARCHAR(255) NOT NULL,
typecarte VARCHAR(30) NOT NULL,
numerocarte VARCHAR(30) NOT NULL
);
INSERT INTO comptes (identifiant, motdepasse, typecarte, numerocarte) VALUES
('user1', 'abc', 'VISA', '987654321987'),
('user2', 'password', 'CB', '096743218765'),
('user3', 'techonthenet', 'CB', '637698341021'),
('user4', 'techonthenet', 'VISA', '990176348956'),
('user5', 'password', 'VISA', '985637980662');
Pour ce TP, les mots de passe sont volontairement enregistrés en clair dans un premier temps
(colonne motdepasse) : nous les hacherons à l'étape 6. Les numéros de carte sont fictifs, mais
retenez qu'en conditions réelles, un site marchand ne doit jamais stocker de vrais numéros de
carte bancaire non chiffrés dans sa base (norme PCI-DSS).
2. Implémentation HTML
Créez le fichier index.html, le formulaire de connexion à l'espace client :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Se connecter - Site marchand</title>
</head>
<body>
<h1>Connexion à votre espace client</h1>
<form action="afficher_numero_carte.php" method="get">
<label for="identifiant">Identifiant :</label>
<input type="text" id="identifiant" name="identifiant"><br/>
<label for="motdepasse">Mot de passe :</label>
<input type="password" id="motdepasse" name="motdepasse"><br/>
<input type="submit" value="Se connecter">
</form>
</body>
</html>
3. Implémentation PHP (sans hachage du mot de passe)
Créez le fichier afficher_numero_carte.php, qui vérifie l'identifiant et le mot de passe, puis
affiche le numéro de carte du client connecté :
<?php
$dsn = "mysql:host=127.0.0.1;port=3306;dbname=site_marchand;charset=utf8mb4";
try {
$pdo = new PDO($dsn, "root", "root", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
} catch (PDOException $e) {
die("Erreur de connexion : " . $e->getMessage());
}
$identifiant = $_GET['identifiant'] ?? '';
$motdepasse = $_GET['motdepasse'] ?? '';
$requeteSQL = "SELECT numerocarte FROM comptes WHERE identifiant = '$identifiant' AND motdepasse = '$motdepasse'";
print("Requête SQL exécutée : $requeteSQL<br>");
$resultat = $pdo->query($requeteSQL)->fetch();
if ($resultat) {
echo "Numéro de carte : " . $resultat['numerocarte'];
} else {
echo "Identifiant ou mot de passe incorrect.";
}
Afficher la requête SQL exécutée (print($requeteSQL)) est utile ici pour comprendre l'attaque
pendant le TP. Sur un vrai site, ne jamais afficher une requête SQL à l'utilisateur : elle révèle
la structure de la base de données à un attaquant.
4. Démarrer le serveur Web
Démarrez le serveur Web intégré à PHP, dans le dossier contenant les deux fichiers :
php -S localhost:8000
Ouvrez ensuite http://localhost:8000/ dans votre navigateur.
5. Tester la connexion de l'utilisateur
- Bon mot de passe :
user1/abc→ le numéro de carte s'affiche. - Mauvais mot de passe :
user1/xyz→ "Identifiant ou mot de passe incorrect." - Injection SQL :
identifiant=' OR '1'='1' --, mot de passe : (vide)
Avec l'injection, un numéro de carte s'affiche à nouveau, alors qu'aucun mot de passe correct
n'a été saisi ! Le -- (suivi d'un espace) transforme la fin de la requête en commentaire SQL, et
'1'='1' est toujours vrai : la clause WHERE est neutralisée, la première ligne de la table
comptes est retournée.
Sous MariaDB/MySQL, le caractère # est aussi accepté comme début de commentaire (jusqu'à la
fin de la ligne). L'attaquant peut donc saisir ' OR '1'='1' # à la place de ' OR '1'='1' -- :
le résultat est identique. Avec #, aucune espace n'est nécessaire derrière le symbole, alors que
-- doit être suivi d'une espace pour être reconnu comme un commentaire.
6. Hashage du mot de passe
Stocker les mots de passe en clair est dangereux : en cas de fuite de la base, tous les mots de
passe des clients seraient directement exploitables. Remplacez-les par leur empreinte, calculée ici
avec la fonction SQL PASSWORD() :
UPDATE comptes SET motdepasse = PASSWORD('abc') WHERE identifiant = 'user1';
UPDATE comptes SET motdepasse = PASSWORD('password') WHERE identifiant IN ('user2', 'user5');
UPDATE comptes SET motdepasse = PASSWORD('techonthenet') WHERE identifiant IN ('user3', 'user4');
Modifiez ensuite afficher_numero_carte.php pour comparer le mot de passe saisi haché de la même
façon, en enveloppant $motdepasse avec PASSWORD() dans la requête :
$requeteSQL = "SELECT numerocarte FROM comptes WHERE identifiant = '$identifiant' AND motdepasse = PASSWORD('$motdepasse')";
PASSWORD() est une fonction historique de MySQL/MariaDB, encore disponible sous MariaDB. Pour une
application PHP moderne, préférez password_hash() et password_verify() avec l'algorithme
bcrypt, plus robuste. PASSWORD() est utilisée ici uniquement parce qu'elle se manipule
directement en SQL, ce qui simplifie ce TP.
7. Tester la connexion de l'utilisateur
Rejouez exactement les mêmes tests qu'à l'étape 5 :
- Bon mot de passe :
user1/abc→ fonctionne toujours (PASSWORD('abc')reproduit le même hachage que celui stocké). - Mauvais mot de passe :
user1/xyz→ échoue toujours. - Injection SQL :
identifiant=' OR '1'='1' --, mot de passe : (vide)
L'injection fonctionne toujours ! Le hachage du mot de passe ne protège que la confidentialité
du mot de passe stocké — il ne change rien à la façon dont $identifiant est concaténé dans la
requête. Le -- continue de supprimer la vérification du mot de passe (haché ou non) : hacher un
mot de passe n'empêche pas une injection SQL.
8. Sécurisation du code PHP avec une requête préparée paramétrée
Modifiez afficher_numero_carte.php pour utiliser une requête préparée, avec bindParam() pour chaque paramètre :
<?php
$dsn = "mysql:host=127.0.0.1;port=3306;dbname=site_marchand;charset=utf8mb4";
try {
$pdo = new PDO($dsn, "root", "root", [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
} catch (PDOException $e) {
die("Erreur de connexion : " . $e->getMessage());
}
$identifiant = $_GET['identifiant'] ?? '';
$motdepasse = $_GET['motdepasse'] ?? '';
$requeteSQL = "SELECT numerocarte FROM comptes WHERE identifiant = :identifiant AND motdepasse = PASSWORD(:motdepasse)";
$stmt = $pdo->prepare($requeteSQL);
$stmt->bindParam(':identifiant', $identifiant);
$stmt->bindParam(':motdepasse', $motdepasse);
$stmt->execute();
$resultat = $stmt->fetch();
if ($resultat) {
echo "Numéro de carte : " . $resultat['numerocarte'];
} else {
echo "Identifiant ou mot de passe incorrect.";
}
bindParam(':identifiant', $identifiant) lie la variable $identifiant au paramètre nommé
:identifiant : PDO lira sa valeur au moment de execute().
La valeur est transmise séparément de la requête SQL, jamais comme du code SQL exécutable.
9. Tester la connexion de l'utilisateur
Rejouez une dernière fois exactement les mêmes tests :
- Bon mot de passe :
user1/abc→ fonctionne toujours. - Mauvais mot de passe :
user1/xyz→ échoue toujours. - Injection SQL :
identifiant=' OR '1'='1' --, mot de passe : (vide)
Résultat attendu : cette fois, l'injection échoue : "Identifiant ou mot de passe incorrect."
10. Explication de la sécurisation
prepare()envoie d'abord la requête SQL au serveur, sans aucune valeur : sa structure (les clausesWHERE,AND...) est donc figée avant même que$identifiantou$motdepassene soient connus.bindParam()associe ensuite chaque valeur à son paramètre nommé (:identifiant,:motdepasse). Le serveur les traite comme de simples données à comparer, jamais comme un morceau de requête SQL.- Résultat : même si
$identifiantcontient' OR '1'='1' --, cette chaîne est comparée telle quelle à la colonneidentifiant— elle ne correspond à aucun compte, et ne peut plus modifier la structure de la requête.
Aux étapes 6 et 7, le hachage protégeait la confidentialité des mots de passe stockés (utile en cas de fuite de la base), mais ne protégeait pas du tout contre l'injection SQL : c'est bien la requête préparée, et elle seule, qui corrige la faille. Les deux protections sont complémentaires, ni l'une ni l'autre ne remplace l'autre.
Notions théoriques
L'injection SQL est l'une des attaques les plus courantes et dangereuses en cybersécurité.
Elle permet à un attaquant d'exécuter des requêtes SQL malveillantes sur une base de données en exploitant une faille dans un formulaire ou une URL mal sécurisée.
Fonctionnement de l'injection SQL
Lorsqu'un site Web interagit avec une base de données, il utilise des requêtes SQL pour récupérer, insérer, modifier ou supprimer des données (CRUD).
Exemple de vulnérabilité
Reprenons le formulaire de connexion du TP :
SELECT numerocarte FROM comptes
WHERE identifiant = '$identifiant'
AND motdepasse = '$motdepasse';
Si l'application insère directement les valeurs de l'utilisateur sans vérification, un attaquant peut entrer :
- Identifiant :
user1' -- - Mot de passe : (vide)
Ce qui donnera la requête suivante :
SELECT numerocarte FROM comptes WHERE identifiant = 'user1' -- ' AND motdepasse = '';
Explication :
-
Le
--(suivi d'une espace) transforme le reste de la requête en commentaire, permettant à l'attaquant de se connecter sans mot de passe, dès qu'il connaît (ou devine) un identifiant valide. -
Sous MariaDB/MySQL, le caractère
#est aussi accepté comme début de commentaire (jusqu'à la fin de la ligne). L'attaquant peut donc entreruser1' #et obtenir le même résultat, sans avoir besoin de l'espace qui doit suivre--:SELECT numerocarte FROM comptes WHERE identifiant = 'user1' #' AND motdepasse = '';
Résultat
L'attaquant accède au compte de user1, sans connaître son mot de passe.
Utiliser ' OR 1=1 --`
Un attaquant peut aussi essayer de contourner l'authentification sans connaître aucun identifiant valide, en entrant :
- Identifiant :
' OR 1=1 -- - Mot de passe : (vide)
Cette entr ée modifie la requête SQL comme suit :
SELECT numerocarte FROM comptes WHERE identifiant = '' OR 1=1 -- ' AND motdepasse = '';
Explication :
OR 1=1est toujours vrai, donc la condition est validée pour tous les comptes.--(suivi d'une espace) transforme le reste en commentaire, supprimant la vérification du mot de passe.- Sous MariaDB/MySQL,
' OR 1=1 #fonctionne aussi, car#est également accepté comme commentaire.
Résultat
L'attaquant est connecté en tant que premier compte de la table (souvent le plus ancien, parfois un compte administrateur).
Le hachage ne protège pas contre l'injection
Une confusion fréquente est de croire que hacher les mots de passe (étapes 6 et 7 du TP) protège
aussi contre l'injection SQL. Ce n'est pas le cas : l'injection agit sur la colonne
identifiant et sur la structure même de la requête (via -- ou OR), pas sur la comparaison du
mot de passe. Hacher protège une donnée stockée, une requête préparée protège une requête
exécutée : ce sont deux défenses différentes, contre deux risques différents.
Comment se protéger ?
- Utilisation des requêtes préparées (
prepare()+bindParam()) pour éviter l'injection. - Hachage des mots de passe (
PASSWORD()en SQL, ou mieuxpassword_hash()/password_verify()en PHP) pour protéger leur confidentialité en cas de fuite de données. - Validation des entrées : vérifier et filtrer le format des données saisies (ex :
filter_var()pour un email), en complément, jamais en remplacement, des requêtes pr éparées. - Utilisation de privilèges limités : ne pas donner trop de droits aux utilisateurs de la base de données.
- Désactivation des erreurs SQL visibles : ne pas afficher les messages d'erreur SQL à l'utilisateur.
Simulation d'une attaque
Source : https://guerinludovic.forge.apps.education.fr/simulateur-injection-sql/