Toute instruction SQL construite en assemblant des chaînes avec des valeurs non fiables peut être injectée, et le correctif est le même quel que soit le langage : garder le SQL fixe et lier les valeurs sous forme de paramètres. L'OWASP Query Parameterization Cheat Sheet classe l'injection SQL dans A05:2025-Injection de l'OWASP Top 10:2025 et la décrit comme l'une des vulnérabilités web les plus dangereuses.
Pourquoi la concaténation échoue
En SQL, il n'y a pas de véritable distinction entre le plan de contrôle et le plan de données. Un attaquant place un métacaractère dans une entrée, et des commandes SQL apparaissent là où il n'y en avait pas. La page communautaire de l'OWASP sur l'injection SQL présente une requête C# construite par concaténation :
string query = "SELECT * FROM items WHERE owner = '"
+ userName + "' AND itemname = '"
+ ItemName.Text + "'";
Si l'attaquant saisit name' OR 'a'='a comme nom d'article, la condition est toujours vraie. La requête renvoie alors toutes les lignes de la table items, quel que soit leur propriétaire. Selon les fonctionnalités et les privilèges de la base, la même faille peut aussi modifier des données, toucher à des fichiers ou exécuter des commandes du système d'exploitation.
L'origine de la valeur n'a pas d'importance. Les mesures s'appliquent que la donnée provienne d'une requête HTTP, d'un autre service ou de données déjà stockées.
Trouver les requêtes construites par chaînes
Commencez par un grep des mots-clés SQL situés près d'une concaténation ou d'un formatage. Ces motifs ne sont qu'une première passe approximative, pas un scanner. Ils génèrent du bruit et ratent les requêtes réparties sur plusieurs lignes.
# concatenation (Java, C#, JavaScript, PHP uses "." instead of "+")
grep -rnEi "(select|insert into|update|delete from)[^;]*[\"'] *\+" src/
# Python f-strings and .format()
grep -rnEi "f[\"'](select|insert|update|delete)|(select|insert|update|delete).*\.format\(" .
# JavaScript template literals
grep -rnEi '`(select|insert|update|delete)[^`]*\$\{' src/
# sprintf-style formatting
grep -rnEi "sprintf\(.*(select|insert|update|delete)" .
Lisez chaque résultat et vérifiez que les valeurs non fiables sont liées et non concaténées. Examinez ensuite les endroits que grep a tendance à manquer :
- Les chaînes de requête d'ORM. Les requêtes Hibernate peuvent être vulnérables lorsque des valeurs sont concaténées dans la chaîne de requête au lieu d'être liées.
- Les procédures stockées. Elles gardent le SQL dans la base de données, mais ne sont pas sûres automatiquement. L'OWASP présente une procédure PL/SQL qui assemble
usretitmdans une chaîne de requête et indique qu'elle est « vulnerable to the same SQL injection attack shown in the first example » (vulnérable à la même attaque par injection SQL que celle du premier exemple). Lier les arguments de l'appel protège l'appel, pas le SQL dynamique construit à l'intérieur de la procédure. - La paramétrisation côté client. De nombreuses bibliothèques côté client construisent les requêtes par concaténation de chaînes et envoient la requête brute au serveur. Vérifiez que la paramétrisation a bien lieu côté serveur.
- Les identifiants dynamiques et les sens de tri. Ils ne peuvent pas être liés, et c'est donc là que la concaténation survit à une campagne de paramétrisation.
Puis testez. Le Web Security Testing Guide de l'OWASP propose des tests d'injection SQL, manuels et automatisés, y compris des techniques en aveugle (blind) et out-of-band. Pour un contrôle externe de ce que votre revue a laissé passer, c'est le rôle d'un test d'intrusion.
Les remplacer par des paramètres
Définissez la structure SQL séparément de ses valeurs, puis liez les valeurs via l'API de la base de données. Voici l'exemple de l'OWASP SQL Injection Prevention Cheat Sheet, sans le nettoyage des ressources ni la gestion des erreurs :
String query = "SELECT account_balance FROM user_data WHERE user_name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, customerName);
ResultSet results = statement.executeQuery();
Une valeur telle que tom' or '1'='1 est désormais traitée comme le nom d'utilisateur littéral à rechercher. Concaténer une valeur non fiable dans la requête avant de la préparer annule la protection.
Avec un ORM, liez aussi dans la chaîne de requête. Les exemples de l'OWASP utilisent un paramètre nommé en HQL, FROM Inventory i WHERE i.productDescription = :productDescription, et transmettent la valeur avec setParameter.
Pour le SQL dynamique à l'intérieur des procédures stockées, utilisez des variables de liaison (bind variables). En PL/SQL Oracle, cela signifie EXECUTE IMMEDIATE avec USING. Dans SQL Server, c'est sp_executesql avec une liste de paramètres déclarée. Les procédures sans SQL dynamique n'ont besoin de rien de particulier, car leurs paramètres sont déjà liés.
La liaison empêche une valeur de modifier la syntaxe SQL. Elle ne détermine pas si l'appelant a le droit d'accéder au compte concerné : vos contrôles d'autorisation restent donc en place.
Ce que les paramètres ne couvrent pas
Les marqueurs de paramètres représentent des valeurs de données. Ils ne peuvent pas représenter des noms de tables, des noms de colonnes ou des mots-clés comme ASC et DESC. Lorsque les utilisateurs doivent choisir, associez leur sélection à un petit ensemble de jetons propres à l'application et rejetez tout ce qui est inconnu. Avant :
String query = "SELECT id, name FROM items ORDER BY name " + sortParam;
Après :
Map<String, String> directions = Map.of("asc", "ASC", "desc", "DESC");
String direction = directions.get(sortParam);
if (direction == null) {
throw new IllegalArgumentException("Unknown sort direction");
}
String query = "SELECT id, name FROM items ORDER BY name " + direction;
Le texte ajouté à la requête est désormais toujours l'un des deux littéraux, jamais ce que l'utilisateur a envoyé. Continuez à lier chaque valeur de données dans la même requête, même lorsqu'elle passe des contrôles de type, de longueur ou de format.
Ne vous rabattez pas sur l'échappement manuel. Il dépend du dialecte SQL et de la configuration : le mode NO_BACKSLASH_ESCAPES de MySQL, par exemple, désactive l'échappement par barre oblique inverse. L'OWASP déconseille fortement l'échappement et décrit les listes de refus comme pleines de failles.
Limiter les dégâts
Le moindre privilège n'empêche pas l'injection SQL. Il limite ce qu'un attaquant peut faire via un compte de base de données, et doit donc s'appliquer en complément des paramètres.
- Partez de zéro et ajoutez les droits dont chaque compte applicatif a besoin. Chercher ce qu'il faut retirer d'une attribution trop large est la meilleure façon d'oublier des droits.
- Donner un accès DBA ou admin à un compte applicatif est le raccourci habituel, car tout « marche tout seul ». L'OWASP juge cette pratique très dangereuse.
- Une page de connexion a besoin d'un accès en lecture aux champs du nom d'utilisateur et du mot de passe, sans accès en écriture. Une page d'inscription a besoin de l'insertion. Cette séparation n'est applicable que si les deux utilisent des utilisateurs de base de données différents : donnez donc à chaque application web le sien.
- Les attaquants n'ont pas besoin d'injection pour abuser d'un compte trop privilégié. Ils peuvent changer un paramètre d'une valeur légitime vers une valeur pour laquelle ils n'ont pas l'autorisation mais pour laquelle l'application l'a, ce qui explique pourquoi des droits restreints aident même lorsque les requêtes sont saines.
- Une vue peut n'exposer que les champs dont un compte a besoin, avec
SELECTaccordé sur la vue et non sur la table. Vérifiez d'abord les règles de votre base : PostgreSQL contrôle normalement l'accès aux tables sous-jacentes avec les privilèges du propriétaire de la vue. Les requêtes sur des vues nécessitent toujours des paramètres. - Le compte du système d'exploitation compte aussi. N'exécutez pas le SGBD en tant que root ou system. L'OWASP signale que MySQL s'exécute par défaut en tant que system sous Windows.
Par où commencer
- Listez tous les endroits qui construisent du SQL : code applicatif, chaînes de requête d'ORM et procédures stockées utilisant du SQL dynamique. Lancez les motifs grep ci-dessus comme première passe.
- Remplacez les valeurs concaténées par des paramètres liés, côté serveur, et reconstruisez le SQL dynamique des procédures avec des variables de liaison.
- Remplacez les choix dynamiques de table, de colonne et de tri par des correspondances fixes qui rejettent toute entrée inconnue.
- Auditez les comptes de base de données. Retirez les droits DBA et admin des comptes applicatifs, donnez à chaque application son propre utilisateur et basculez l'accès en lecture seule vers des droits restreints ou des vues.
- Exécutez les tests d'injection SQL du Web Security Testing Guide sur le résultat, ou demandez à notre équipe de développement logiciel sécurisé de relire les modifications.
