Injection SQL

- Comprendre comment une application vulnérable peut être attaquée via une injection SQL.
- Apprendre à sécuriser une requête SQL en utilisant PDO avec une requête préparée.
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
Tout d'abord, créez une base de données test_db et une table utilisateurs contenant des identifiants de connexion.
Création de la base de données
CREATE DATABASE test_db;
USE test_db;
Création de la table utilisateurs
CREATE TABLE utilisateurs (
id INT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) NOT NULL,
password VARCHAR(255) NOT NULL
);
Insertion de données
Ajoutez quelques utilisateurs pour tester l'authentification :
INSERT INTO utilisateurs (username, password) VALUES ('admin@gmail.com', 'password123');
INSERT INTO utilisateurs (username, password) VALUES ('prenom.nom@ik.me', 'mypassword');
2. Implémentation PHP
Voici le code du formulaire de login à saisir dans le fichier index.php :
<?php
$dsn = "mysql:host=localhost;dbname=tp-cyber;charset=utf8mb4";
$username = "root";
$password = "root";
try {
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
} catch (PDOException $e) {
die("Erreur de connexion : " . $e->getMessage());
}
$user = $_GET['username'] ?? '';
$pass = $_GET['password'] ?? '';
if ($user != '' && $pass != '') {
print("Tentative de connexion avec l'utilisateur : $user<br>");
$query = "SELECT * FROM utilisateurs WHERE username = '$user' AND password = '$pass'";
print("Requête SQL exécutée : $query<br>");
$result = $pdo->query($query);
if ($result->rowCount() > 0) {
echo "Connexion réussie !";
} else {
echo "Échec de connexion.";
}
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Se connecter</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>Se connecter</h1>
<form method="get" action="">
<label for="username">Nom d'utilisateur:</label>
<input type="text" id="username" name="username" required>
<br>
<label for="password">Mot de passe:</label>
<input type="password" id="password" name="password" required>
<br>
<input type="submit" value="Se connecter">
</form>
<p>Utilisateurs de test :</p>
<ul>
<li>Utilisateur : "<b>admin</b>" | Mot de passe : "<b>password123</b>"</li>
<li>Utilisateur : "<b>user1</b>" | Mot de passe : "<b>mypassword</b>"</li>
<li>Utilisateur : "<b>admin</b>" | Mot de passe : "<b>' OR 1=1 -- </b>"</li>
</ul>
<p>Essayez d'injecter du SQL dans les champs pour voir ce qui se passe !</p>
<br>
<p>Attention, il faut saisir un espace après les 2 tirets du commentaire SQL -- </p>
<br>
<p>Bien sûr, il ne pas utiliser ces techniques sur des systèmes en production !</p>
</body>
</html>
3. Démarrer le serveur Web
Démarrer le serveur Web intégré à PHP, avec la commande :
php -S localhost:8000
4. Tester la connexion de l'utilisateur
Essayez de connecter l'utilisateur avec :
- Utilisateur :
admin' -- - Mot de passe : vide
- Mot de passe : "'; DROP TABLE utilisateurs; --"
- Testez le formulaire HTML avec
POSTau lieu deGETet voyez s'il est toujours vulnérable.
5. Sécurisation du code PHP
Modifiez le script pour utiliser *une requête préparée avec des paramètres nommés.
Voici le code PHP sécurisé :
<?php
$dsn = "mysql:host=localhost;dbname=test_db;charset=utf8mb4";
$username = "root";
$password = "";
try {
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
} catch (PDOException $e) {
die("Erreur de connexion : " . $e->getMessage());
}
$user = $_GET['username'] ?? '';
$pass = $_GET['password'] ?? '';
// Utilisation d'une requête préparée avec des paramètres nommés
$query = "SELECT * FROM utilisateurs WHERE username = :username AND password = :password";
$stmt = $pdo->prepare($query);
$stmt->execute([
'username' => $user,
'password' => $pass
]);
if ($stmt->rowCount() > 0) {
echo "Connexion réussie !";
} else {
echo "Échec de connexion.";
}
?>
6. Explication de la sécurisation
- Utilisation de
prepare(): La requête SQL est préparée à l'avance, empêchant l'injection SQL. - Utilisation de
execute()avec un tableau associatif : Les valeurs sont traitées comme des données et non comme du code SQL. - Protection contre les attaques : Même si un attaquant entre
' OR 1=1 --, la requête ne sera pas modifiée.
7. Test de la sécurité
Essayez à nouveau de connecter l'utilisateur avec une injection SQL.
Résultat attendu : L'injection SQL ne fonctionne plus, et la connexion échoue.
8. Valider/nettoyer les entrées utilisateur
Pour éviter d'insérer directement des entrées utilisateur dans une requête SQL en PHP, ce qui peut mener à des injections SQL, voici les meilleures pratiques :
Avant d'utiliser les données utilisateur, il faut toujours les valider pour s'assurer qu'elles correspondent au format attendu.
Par exemple :
-
Vérifiez que l'email a un format valide avec
filter_var($_POST['email'], FILTER_VALIDATE_EMAIL). https://www.php.net/manual/fr/function.filter-var.php -
Limitez la longueur des chaînes avec
substrou des validations similaires. -
Utilisez des fonctions comme
htmlspecialcharspour éviter les attaques XSS si les données sont affichées (nous verrons cela dans le prochain tutoriel).
Voici un exemple de validation/nettoyage des entrées, à ajouter avant la requête préparée (les identifiants de test étant des adresses email, comme dans les données insérées à l'étape 1) :
<?php
$user = $_GET['username'] ?? '';
$pass = $_GET['password'] ?? '';
// 1. Valider le format de l'email avec filter_var()
if (!filter_var($user, FILTER_VALIDATE_EMAIL)) {
die("Le nom d'utilisateur doit être une adresse email valide.");
}
// 2. Limiter la longueur des chaînes avec substr(), pour éviter des saisies excessivement longues
$user = substr($user, 0, 100);
$pass = substr($pass, 0, 100);
// La requête SQL utilise ensuite $user et $pass avec une requête préparée (voir étape 5) :
// c'est elle qui protège réellement contre l'injection SQL, pas la validation ci-dessus.
filter_var() et substr() valident le format des données (défense en profondeur), mais ne
protègent pas, à elles seules, contre l'injection SQL. Seule la requête préparée (étape 5) empêche
réellement qu'une entrée soit interprétée comme du code SQL.
Par principe, pour des raisons de sécurité, il ne faut jamais insérer directement des entrées utilisateur dans une requête SQL.
Valider et nettoyer les entrées utilisateur est une bonne pratique, mais n'est pas suffisante. Utilisez toujours les requêtes préparées.
9. Hashage du mots de passe
Actuellement, les mots de passe sont stockés en clair.
Pour une meilleure sécurité, utilisez bcrypt avec
password_hash()etpassword_verify().
Modification de l'insertion des utilisateurs
$hashed_password = password_hash('password123', PASSWORD_BCRYPT);
$query = "INSERT INTO utilisateurs (username, password) VALUES (:username, :password)";
$stmt = $pdo->prepare($query);
$stmt->execute([
'username' => 'admin',
'password' => $hashed_password
]);
Modification de la vérification du mot de passe
$query = "SELECT * FROM utilisateurs WHERE username = :username";
$stmt = $pdo->prepare($query);
$stmt->execute(['username' => $user]);
$userData = $stmt->fetch(PDO::FETCH_ASSOC);
if ($userData && password_verify($pass, $userData['password'])) {
echo "Connexion réussie !";
} else {
echo "Échec de connexion.";
}
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é
Prenons un site avec un formulaire de connexion :
SELECT * FROM utilisateurs
WHERE username = '$utilisateur'
AND password = '$mot_de_passe';
Si l'application insère directement les valeurs de l'utilisateur sans vérification, un attaquant peut entrer :
- Nom d'utilisateur :
admin' -- - Mot de passe : (vide)
Ce qui donnera la requête suivante :
SELECT * FROM utilisateurs WHERE username = 'admin' --' AND password = '';
Explication :
- Le
--transforme le reste de la requête en commentaire, permettant à l'attaquant de se connecter sans mot de passe.
Résultat
L'attaquant est connecté en tant que premier utilisateur de la base de données, souvent un administrateur.
Utiliser ' OR 1=1 --`
Un attaquant peut aussi essayer de contourner l'authentification en entrant :
- Nom d'utilisateur :
' OR 1=1 -- - Mot de passe : (vide)
Cette entrée modifie la requête SQL comme suit :
SELECT * FROM utilisateurs WHERE username = '' OR 1=1 --' AND password = '';
Explication :
OR 1=1est toujours vrai, donc la condition est validée pour tous les utilisateurs.--transforme le reste en commentaire, supprimant la vérification du mot de passe.
Résultat
L'attaquant est connecté en tant que premier utilisateur de la base de données, souvent un administrateur.
Comment se protéger ?
- Utilisation des requêtes préparées pour éviter l'injection.
- Validation des entrées : Vérifier et filtrer les données saisies par l'utilisateur.
- 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/
Test de mémorisation/compréhension
Bonnes pratiques
- Utilisation des requêtes préparées pour éviter l'injection.
- Validation des entrées : Vérifier et filtrer les données saisies par l'utilisateur.
- 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.
htmlspecialchars() protège contre les attaques XSS lors de l'affichage, pas contre les
injections SQL. Ne l'utilisez jamais comme substitut aux requêtes préparées.