← Cybersécurité

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 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 :

  1. 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.
  2. 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.

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