Injection SQL
Comprendre et exploiter cette vulnérabilité
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).
Si les requêtes SQL ne sont pas correctement sécurisées, un attaquant peut injecter du code SQL malveillant pour manipuler la base de données.
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 = '';
Le --
transforme le reste de la requête en commentaire, permettant à l'attaquant de se connecter sans mot de passe.
Comment se protéger ?
- Utilisation des requêtes préparées (
Prepared Statements
) 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.
Exemple pratique
Exploitation d'une injection SQL
Nous allons voir comment une simple injection SQL peut permettre d'accéder à une base de données non sécurisée.
Contexte
Un site Web possède un formulaire de connexion avec cette requête SQL vulnérable :
SELECT * FROM utilisateurs WHERE username = '$utilisateur' AND password = '$mot_de_passe';
Un attaquant peut 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=1
est 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.
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Objectif
- 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 des paramètres nommés.
1. Création de la base de données et de la table utilisateurs
Tout d'abord, créez une base de données 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', 'password123');
INSERT INTO utilisateurs (username, password) VALUES ('user1', 'mypassword');
2. Script PHP (sans formulaire)
Ce script récupère les entrées de l'utilisateur via l'URL et exécute une requête SQL.
Code PHP
<?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'] ?? '';
$query = "SELECT * FROM utilisateurs WHERE username = '$user' AND password = '$pass'";
$result = $pdo->query($query);
if ($result->rowCount() > 0) {
echo "Connexion réussie !";
} else {
echo "Échec de connexion.";
}
?>
Test d'une injection SQL
Essayez d'accéder à l'URL suivante :
http://localhost/login.php?username=admin'--&password=
Cela contourne l'authentification et connecte l'attaquant en tant qu'admin.
3. Script PHP (avec formulaire)
Ce script récupère les entrées de l'utilisateur via un formulaire et exécute une requête SQL.
Code 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>TP Cyber Sécurité</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>TP Cyber Sécurité</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>
- Testez le formulaire HTML avec
POST
au lieu deGET
et voyez s'il est toujours vulnérabilité. - Testez avec :
- Utilisateur : "admin"
- Mot de passe : "**'; DROP TABLE utilisateurs; -- **"
3. Sécurisation avec une requête préparée
Modifiez le script pour utiliser une requête préparée avec des paramètres nommés.
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.";
}
?>
4. 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.
5. Test de la sécurité
Essayez à nouveau d'accéder à l'URL :
http://localhost/login.php?username=admin'--&password=
Résultat attendu : L'injection SQL ne fonctionne plus, et la connexion échoue.
6. Amélioration : Valider et 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
substr
ou des validations similaires. -
Utilisez des fonctions comme
htmlspecialchars
pour éviter les attaques XSS si les données sont affichées (nous verrons cela dans le prochain tutoriel).
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.
7. Amélioration : Hashage des 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.";
}
- Ne jamais insérer directement des entrées utilisateur dans une requête SQL.
- Toujours utiliser des requêtes préparées avec PDO.
- Stocker les mots de passe de manière sécurisée avec
password_hash()
.