Aller au contenu principal

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).

attention

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


Quelle est la principale cause d'une injection SQL ?


Que permet une injection SQL ?


Quel caractère est souvent utilisé pour commenter une requête SQL et contourner la sécurité ?


Quelle est la meilleure méthode pour éviter l'injection SQL ?


Pourquoi une requête SQL contenant `OR 1=1` est-elle problématique ?


Quel est l'effet d'une injection SQL sur un formulaire de connexion vulnérable ?


Pourquoi ne faut-il pas afficher les erreurs SQL aux utilisateurs ?


Quel rôle joue `--` dans une injection SQL ?



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>
info
  • Testez le formulaire HTML avec POST au lieu de GET 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).

info

Par principe, pour des raisons de sécurité, il ne faut jamais insérer directement des entrées utilisateur dans une requête SQL.

attention

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() et password_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.";
}
Points clés
  • 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().