Guide du débutant sur l'injection SQL (et comment s'en protéger)
Vous développez une fonctionnalité de recherche pour votre application. Les utilisateurs saisissent un nom de produit, votre backend interroge la base de données et les résultats s’affichent. Assez simple—jusqu’à ce que quelqu’un tape '; DROP TABLE products;-- dans cette zone de recherche.
Il s’agit d’une injection SQL, et elle reste l’une des vulnérabilités les plus dangereuses en développement web. Le OWASP Top 10 classe « Injection » comme un risque de sécurité critique, et l’injection SQL se trouve au cœur de cette catégorie. Si vous écrivez du code qui interagit avec une base de données—même occasionnellement—vous devez comprendre comment fonctionne cette attaque et comment l’arrêter.
Points clés à retenir
- L’injection SQL se produit lorsque des attaquants manipulent les requêtes de base de données en insérant du code malveillant via les entrées utilisateur
- Les requêtes paramétrées (prepared statements) constituent la défense principale, en séparant le code SQL des données
- Les ORM ne sont pas automatiquement sûrs lors de l’utilisation de méthodes de requêtes brutes
- Utilisez des listes blanches pour les identifiants dynamiques comme les noms de tables et de colonnes, qui ne peuvent pas être paramétrés
- Appliquez une défense en profondeur : comptes à privilèges minimaux, couches d’accès aux données centralisées et tests de sécurité dans les pipelines CI
Qu’est-ce que l’injection SQL ?
L’injection SQL se produit lorsqu’un attaquant manipule les requêtes de base de données de votre application en insérant du code malveillant via les entrées utilisateur. Au lieu de traiter l’entrée comme des données, la base de données l’exécute comme des commandes.
Prenons l’exemple d’un formulaire de connexion. Votre backend pourrait construire une requête comme celle-ci :
SELECT * FROM users WHERE email = 'user@example.com' AND password = 'secret123'
Si vous construisez cette requête en concaténant des chaînes avec l’entrée utilisateur, un attaquant peut saisir ' OR '1'='1 comme mot de passe. La requête résultante devient :
SELECT * FROM users WHERE email = 'user@example.com' AND password = '' OR '1'='1'
Puisque '1'='1' est toujours vrai, la requête renvoie tous les utilisateurs, contournant complètement l’authentification.
Pourquoi l’injection SQL reste importante
Vous pourriez supposer que les frameworks modernes ont résolu ce problème. Ce n’est pas le cas—du moins pas automatiquement.
Les vulnérabilités d’injection SQL apparaissent dans :
- Les endpoints d’API acceptant du JSON avec des paramètres de filtre
- Les formulaires de recherche avec plusieurs options de requête
- Les tableaux de bord d’administration avec tri ou filtrage dynamique
- Les générateurs de rapports avec des critères définis par l’utilisateur
Tout endroit où l’entrée utilisateur influence une requête de base de données constitue une surface d’attaque potentielle. Les conséquences vont du vol de données à la destruction complète de la base de données.
Comment se produit la construction de requêtes non sécurisées
La cause fondamentale est toujours la même : traiter l’entrée utilisateur comme du code SQL de confiance plutôt que comme des données.
La concaténation de chaînes est le principal coupable :
// Vulnérable - ne faites jamais cela
const query = `SELECT * FROM products WHERE name = '${userInput}'`;
Les template literals et le formatage de chaînes créent le même problème :
# Vulnérable - ne faites jamais cela
query = f"SELECT * FROM products WHERE category = '{category}'"
Même les ORM ne sont pas automatiquement sûrs lorsque vous utilisez des méthodes de requêtes brutes :
// Vulnérable - les requêtes brutes contournent les protections de l'ORM
db.query(`SELECT * FROM users WHERE id = ${req.params.id}`);
Prévention de l’injection SQL avec les requêtes paramétrées
La défense principale contre l’injection SQL consiste à utiliser des requêtes paramétrées (également appelées prepared statements). Celles-ci séparent le code SQL des données, garantissant que l’entrée utilisateur n’est jamais exécutée comme des commandes.
Voici l’approche sécurisée en JavaScript :
// Sécurisé - requête paramétrée
const query = 'SELECT * FROM products WHERE name = ?';
db.query(query, [userInput]);
Et en Python :
# Sécurisé - requête paramétrée
cursor.execute("SELECT * FROM products WHERE name = %s", (user_input,))
La base de données traite le paramètre comme une valeur littérale, et non comme du code SQL. Même si quelqu’un saisit '; DROP TABLE products;--, la base de données recherche un produit ayant exactement cette chaîne comme nom.
Ce que les requêtes paramétrées ne couvrent pas
Les identifiants (noms de tables, noms de colonnes) ne peuvent pas être paramétrés. Si vous avez besoin d’un tri dynamique :
// Utilisez une liste blanche pour les noms de colonnes
const allowedColumns = ['name', 'price', 'created_at'];
const sortColumn = allowedColumns.includes(userInput) ? userInput : 'name';
const query = `SELECT * FROM products ORDER BY ${sortColumn}`;
Les procédures stockées ne sont sûres que si elles évitent le SQL dynamique en interne. Une procédure stockée qui concatène des chaînes est tout aussi vulnérable.
Pourquoi l’échappement ne suffit pas
L’échappement manuel de chaînes et la désinfection sont insuffisants en eux-mêmes. Ils sont sujets aux erreurs, spécifiques à chaque base de données et faciles à implémenter incorrectement. Les requêtes paramétrées gèrent l’échappement automatiquement et correctement.
Discover how at OpenReplay.com.
Défense en profondeur
Les requêtes paramétrées constituent votre défense principale, mais des couches supplémentaires aident :
- Comptes de base de données à privilèges minimaux : l’utilisateur de base de données de votre application ne devrait pas avoir de permissions administratives telles que DROP, ALTER ou CREATE USER en production
- Couche d’accès aux données centralisée : acheminez toutes les requêtes via un module unique qui impose la paramétrisation
- Revue de code et tests : incluez des vérifications d’injection SQL dans votre processus de revue et votre pipeline CI
Les recommandations modernes de l’OWASP ASVS et de l’initiative « Secure by Design » de la CISA mettent l’accent sur l’intégration de la sécurité dans votre processus de développement plutôt que de l’ajouter après coup.
Conclusion
L’injection SQL reste dangereuse car elle est facile à introduire et dévastatrice lorsqu’elle est exploitée. La solution est simple : utilisez des requêtes paramétrées pour toutes les opérations de base de données, validez les identifiants avec des listes blanches et ne faites jamais confiance aux entrées utilisateur.
Faites des requêtes paramétrées votre approche par défaut. Votre futur vous-même—et vos utilisateurs—vous en remercieront.
FAQ
Oui. L'injection NoSQL est une vulnérabilité connexe affectant les bases de données comme MongoDB. Les attaquants peuvent manipuler les opérateurs de requête via les entrées utilisateur. La défense est similaire : utilisez les méthodes de requête intégrées du pilote de base de données plutôt que de construire des requêtes à partir de chaînes, et validez toutes les entrées utilisateur avant de les utiliser dans les requêtes.
Les ORM offrent une protection lorsque vous utilisez leurs méthodes de requête standard. Cependant, la plupart des ORM proposent des fonctions de requêtes brutes qui contournent ces protections. Si vous utilisez des méthodes SQL brutes ou l'interpolation de chaînes dans les requêtes ORM, vous êtes toujours vulnérable. Utilisez toujours des méthodes paramétrées, même dans le code ORM.
Utilisez des outils automatisés comme SQLMap ou OWASP ZAP pour rechercher les vulnérabilités. Incluez des tests unitaires axés sur la sécurité qui tentent des payloads d'injection. Effectuez des revues de code vérifiant spécifiquement la concaténation de chaînes dans les requêtes. Envisagez des tests d'intrusion pour les applications critiques avant le déploiement en production.
Non. La validation des entrées aide à réduire la surface d'attaque mais ne devrait jamais être votre seule défense. Les attaquants habiles peuvent souvent contourner les règles de validation. Les requêtes paramétrées constituent la défense principale car elles séparent fondamentalement le code des données. Utilisez la validation comme une couche supplémentaire, et non comme un remplacement d'une construction de requête appropriée.
Gain control over your UX
See how users are using your site as if you were sitting next to them, learn and iterate faster with OpenReplay. — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.