Bonjour ! Dans la leçon précédente, nous avons appris à établir et à gérer proprement une connexion à notre base de données MySQL en utilisant DriverManager et try-with-resources. Nous avons maintenant un canal de communication fiable.
Aujourd'hui, nous allons utiliser cette connexion pour sa raison d'être : exécuter des requêtes SQL. Cependant, il ne suffit pas de les exécuter ; il faut le faire de manière sécurisée. L'objectif de cette leçon est d'apprendre à utiliser l'objet PreparedStatement pour exécuter des requêtes de manière sûre et efficace, en nous protégeant contre l'une des failles de sécurité les plus courantes et les plus dangereuses : l'injection SQL.
1. Le danger : l'injection SQL
Imaginez que vous deviez vérifier les identifiants d'un utilisateur. L'approche la plus naïve serait de construire la requête SQL en concaténant directement les chaînes de caractères fournies par l'utilisateur (son login et son mot de passe).
// ATTENTION : CODE VULNÉRABLE !
String login = //... entrée utilisateur
String password = //... entrée utilisateur
String sql = "SELECT * FROM users WHERE login = '" + login + "' AND password = '" + password + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql); // Exécution de la requête concaténée
À première vue, cela semble fonctionner. Mais que se passe-t-il si un utilisateur malveillant saisit quelque chose de spécial dans le champ du login ? Par exemple : admin' --.
La requête SQL deviendrait :SELECT * FROM users WHERE login = 'admin' -- ' AND password = '...'
En SQL, -- est un commentaire. Tout ce qui suit est ignoré. La base de données exécute donc SELECT * FROM users WHERE login = 'admin', ignorant complètement la vérification du mot de passe. L'attaquant vient de se connecter en tant qu'administrateur sans connaître le mot de passe. C'est une injection SQL.
Pour voir concrètement l'impact dévastateur de cette faille, regardez la vidéo suivante.
TUTO Java JDBC - Eviter les attaques par Injection SQL
Cette vidéo de Dominique Liard montre de manière très claire comment une application simple peut être compromise par une injection SQL.
Concentrez-vous sur les deux démonstrations d'attaques : Bypass d'authentification : Voyez comment un attaquant peut se connecter en tant qu'un autre utilisateur sans connaître son mot de passe. Usurpation d'identité admin : Observez une attaque encore plus grave qui permet de se connecter en tant que premier utilisateur de la base de données (souvent l'administrateur). L'auteur montre même qu'il serait possible d'exécuter des commandes destructrices, comme DROP TABLE.
Cette vulnérabilité provient du fait que nous avons mélangé le code (la structure de la requête SQL) et les données (l'entrée de l'utilisateur) dans une seule chaîne de caractères. La solution est de les séparer.
2. La solution : PreparedStatement
L'API JDBC fournit une solution robuste à ce problème : l'interface PreparedStatement. Le principe est simple mais puissant :
- Vous écrivez votre requête SQL comme un modèle (template), en utilisant des points d'interrogation (
?) comme espaces réservés (placeholders) pour les valeurs dynamiques. - Vous fournissez ensuite les valeurs pour ces paramètres séparément, via des méthodes spécifiques comme
setString(),setInt(), etc. - Le pilote JDBC s'assure que ces valeurs sont traitées uniquement comme des données. Même si une valeur contient des caractères SQL spéciaux (comme
'ou--), ils seront "échappés" et ne pourront jamais modifier la structure de la requête SQL.
Le diagramme ci-dessous illustre ce flux sécurisé.

La comparaison de code suivante rend la différence très claire.

Pour une explication textuelle détaillée, consultez la ressource suivante.
Accès aux bases de données : JDBC — Java EE
Cette documentation explique de manière concise et technique le fonctionnement des requêtes préparées.
Lisez les sections suivantes : "Le PreparedStatement" : Pour comprendre comment créer un PreparedStatement et comment définir ses paramètres avec les méthodes setXXX(). Notez le détail important : l'index des paramètres commence à 1, pas à 0. "L’injection SQL" : Cette partie contient un exemple de code avant/après qui résume parfaitement le problème et sa solution avec PreparedStatement.
3. Mettre en œuvre le PreparedStatement
Voyons maintenant comment transformer le code vulnérable en code sécurisé.
Le processus se déroule en trois étapes :
- Préparer la requête : On utilise
connection.prepareStatement(sql)avec une requête contenant des?. - Lier les paramètres : On appelle
pstmt.setXXX(index, valeur)pour chaque?.XXXcorrespond au type de données (String,Int,Date, etc.). - Exécuter la requête : On appelle
pstmt.executeQuery()(pour unSELECT) oupstmt.executeUpdate()(pourINSERT,UPDATE,DELETE). Notez que ces méthodes sont appelées sans argument, car la requête a déjà été fournie lors de la préparation.
La vidéo ci-dessous vous montre exactement comment refactoriser le code vulnérable vu précédemment pour utiliser PreparedStatement.
TUTO Java JDBC - Eviter les attaques par Injection SQL
Nous reprenons la même vidéo pour voir l'implémentation de la solution et la vérification de son efficacité.
Regardez attentivement ces deux parties : Correction avec PreparedStatement : L'auteur modifie son code pour utiliser connection.prepareStatement(), les placeholders ?, et les méthodes setString() et setInt(). C'est l'implémentation concrète de ce que nous venons d'apprendre. Test de la solution : Les tentatives d'injection SQL qui fonctionnaient auparavant échouent maintenant, prouvant que la solution est efficace.
4. Exercice Pratique : Sécuriser une méthode d'authentification
Il est temps de mettre cela en pratique. Nous allons reprendre le projet de la leçon précédente et y ajouter une méthode de vérification d'utilisateur sécurisée.
-
Ouvrez votre projet Maven. Assurez-vous que votre base de données contient une table
usersavec des colonnesloginetpassword. Si ce n'est pas le cas, créez-la et insérez quelques utilisateurs.CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, login VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(50) NOT NULL ); INSERT INTO users (login, password) VALUES ('jdoe', 'pass123'), ('admin', 'adminpass'); -
Dans votre classe
Main, ajoutez la méthodecheckUsersuivante. Pour l'instant, elle est vide.public static boolean checkUser(Connection connection, String login, String password) throws SQLException { // TODO: Implémentez la logique de vérification sécurisée ici return false; } -
Votre tâche : Complétez la méthode
checkUseren utilisant unPreparedStatementpour vérifier si un utilisateur avec leloginet lepasswordfournis existe dans la base de données. La méthode doit retournertruesi l'utilisateur existe,falsesinon. -
Modifiez votre méthode
mainpour appelercheckUserà l'intérieur du bloctry-with-resourcesde la connexion, et testez différents scénarios.// Dans votre méthode main, à l'intérieur du try-with-resources de la connexion try (Connection connection = DriverManager.getConnection(url, user, password)) { System.out.println("Connexion établie avec succès !"); // Scénario 1: Identifiants corrects boolean isUser1Valid = checkUser(connection, "jdoe", "pass123"); System.out.println("Tentative de connexion pour jdoe: " + (isUser1Valid ? "Succès" : "Échec")); // Scénario 2: Mot de passe incorrect boolean isUser2Valid = checkUser(connection, "jdoe", "wrongpassword"); System.out.println("Tentative de connexion pour jdoe avec mauvais mdp: " + (isUser2Valid ? "Succès" : "Échec")); // Scénario 3: Tentative d'injection SQL boolean isInjectionValid = checkUser(connection, "admin' --", "garbage"); System.out.println("Tentative d'injection SQL: " + (isInjectionValid ? "Succès (FAILLE !)" : "Échec (SÉCURISÉ !)")); } catch (SQLException e) { // ... }
Cliquez ici pour voir la solution
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
// ... autres imports
public class Main {
public static boolean checkUser(Connection connection, String login, String password) throws SQLException {
// 1. Définir la requête avec des placeholders
String sql = "SELECT * FROM users WHERE login = ? AND password = ?";
// 2. Créer un PreparedStatement dans un try-with-resources pour une fermeture automatique
try (PreparedStatement pstmt = connection.prepareStatement(sql)) {
// 3. Lier les paramètres
pstmt.setString(1, login); // Le premier '?' est le login
pstmt.setString(2, password); // Le deuxième '?' est le mot de passe
System.out.println("Exécution de la requête : " + pstmt);
// 4. Exécuter la requête et obtenir le ResultSet
try (ResultSet rs = pstmt.executeQuery()) {
// 5. Vérifier si le ResultSet contient au moins une ligne
// rs.next() déplace le curseur sur la première ligne et retourne 'true' si elle existe.
return rs.next();
}
}
}
public static void main(String[] args) {
// ... votre code de chargement des propriétés et de connexion ...
try (Connection connection = DriverManager.getConnection(url, user, password)) {
System.out.println("Connexion établie avec succès !");
// Scénario 1: Identifiants corrects
boolean isUser1Valid = checkUser(connection, "jdoe", "pass123");
System.out.println("Tentative de connexion pour jdoe: " + (isUser1Valid ? "Succès" : "Échec"));
// Scénario 2: Mot de passe incorrect
boolean isUser2Valid = checkUser(connection, "jdoe", "wrongpassword");
System.out.println("Tentative de connexion pour jdoe avec mauvais mdp: " + (isUser2Valid ? "Succès" : "Échec"));
// Scénario 3: Tentative d'injection SQL
boolean isInjectionValid = checkUser(connection, "admin' -- ", "garbage");
System.out.println("Tentative d'injection SQL: " + (isInjectionValid ? "Succès (FAILLE !)" : "Échec (SÉCURISÉ !)"));
} catch (SQLException e) {
System.err.println("Échec de la connexion : " + e.getMessage());
e.printStackTrace();
}
}
}
Conclusion
Félicitations ! Vous venez de maîtriser une compétence absolument fondamentale et non négociable pour tout développeur backend : la protection contre les injections SQL.
Points clés à retenir :
- La concaténation de chaînes de caractères pour construire des requêtes SQL à partir d'entrées utilisateur est extrêmement dangereuse et doit être bannie de votre code.
- Le
PreparedStatementest la solution standard et robuste pour exécuter des requêtes dynamiques en toute sécurité. - Le processus implique la création d'une requête "modèle" avec des
?, la liaison des paramètres avec les méthodessetXXX(), et enfin l'exécution de la requête préparée.
Maintenant que nous savons envoyer des requêtes de manière sécurisée, il nous reste une étape cruciale : comment lire et traiter efficacement les données que la base de données nous renvoie ? Ce sera l'objet de notre prochaine leçon, où nous explorerons en profondeur le ResultSet.