Qu'est-ce que l'injection SQL et comment l'éviter en C# ?
L'injection SQL est une technique d'injection de code qui permet aux attaquants d'envoyer un code SQL malveillant à votre serveur de base de données par l'intermédiaire d'une entrée utilisateur. Dans sa vidéo "What Is SQL Injection And How Do I Prevent It in C#?", Tim Corey démontre exactement comment les vulnérabilités d'injection SQL apparaissent dans le code réel, montre plusieurs exemples d'attaques d'injection SQL réussies (y compris les attaques basées sur l'union et les attaques destructives), et présente des techniques pratiques de prévention de l'injection SQL que vous pouvez appliquer en C#. Cet article suit le cheminement de Tim afin que vous puissiez voir exactement les problèmes et les solutions qu'il présente.
L'application démo et son importance
Tim commence par une petite application de démonstration WPF liée à une InjectableDB locale (tables People et Secrets). La boîte de recherche de l'application, qui s'apparente à un formulaire web, prend en compte les données de l'utilisateur (un nom de famille) et construit une requête SQL qui renvoie l'ID, le prénom et le nom de famille. Cela "fonctionne" - tapez Corey et vous obtenez Tim Corey - mais Tim insiste sur le point essentiel : "Ce n'est pas parce qu'une application fonctionne qu'elle est sûre" Une application web qui fonctionne peut toujours présenter des vulnérabilités par injection SQL lorsque les données fournies par l'utilisateur sont insérées directement dans des instructions SQL par le biais de la concaténation de chaînes de caractères ou de SQL dynamique.
Le code non sécurisé - concaténation de chaînes de caractères et SQL dynamique
Tim montre le modèle non sécurisé exact utilisé par de nombreux développeurs :
var sql = $"SELECT * FROM People WHERE LastName = '{searchText}'";
var results = connection.Query<Person>(sql);
var sql = $"SELECT * FROM People WHERE LastName = '{searchText}'";
var results = connection.Query<Person>(sql);
Cette requête originale utilise la concaténation de chaînes pour créer une instruction SQL. Tim met en garde : si vous voyez du code qui injecte des données utilisateur directement dans des requêtes SQL, arrêtez-vous - il s'agit d'une vulnérabilité d'injection SQL. Les attaquants peuvent créer des entrées malveillantes qui modifient la structure de vos commandes SQL ou même exécuter des instructions SQL malveillantes supplémentaires.
Comment un attaquant l'exploite - UNION et DROP
Pour montrer comment fonctionne une attaque par injection SQL, Tim reproduit des requêtes dans SQL Server, puis réalise des injections en utilisant UNION ALL et des commentaires SQL (--) pour masquer les caractères de fin. Exemples de charges utiles malveillantes que Tim démontre :
-
Injection SQL basée sur l'union pour lire d'autres tables :
UNION ALL SELECT ID, Username AS FirstName, Password AS LastName FROM Secrets ;Cela mélange les résultats de Secrets à l'ensemble des résultats SELECT d'origine, exposant ainsi des données sensibles telles que les noms d'utilisateur et les mots de passe.
-
Injection destructive pour supprimer des tables :
DROP TABLE DemoTable ;Cela permet d'exécuter une deuxième instruction SQL (DROP TABLE) en terminant la première instruction par un point-virgule, puis en ajoutant la commande destructive. Tim montre que la table disparaît - la base de données a été modifiée par un SQL malveillant.
Le point de vue de Tim : les attaquants n'ont pas besoin de connaître les noms des tables ou des colonnes à l'avance - ils peuvent énumérer les noms des tables ou des colonnes à partir des serveurs de base de données, ou simplement essayer des techniques aveugles ou basées sur le temps pour découvrir le comportement.
Réparation 1 - Requêtes paramétrées
La première et principale défense de Tim est d'arrêter de construire des chaînes SQL avec les données de l'utilisateur. Remplacer le SQL dynamique par des requêtes paramétrées :
string sql = "SELECT * FROM People WHERE LastName = @LastName";
var results = connection.Query<Person>(sql, new { LastName = searchText });
string sql = "SELECT * FROM People WHERE LastName = @LastName";
var results = connection.Query<Person>(sql, new { LastName = searchText });
Tim explique que la paramétrisation (utilisation du style d'instruction préparée) signifie que la base de données traite l'entrée fournie par l'utilisateur strictement comme des données - le SQL malveillant devient juste une valeur de chaîne et ne peut pas changer la structure SQL. Cela permet d'éviter de nombreuses attaques courantes par injection SQL, notamment les charges utiles basées sur l'union et les fichiers .NET annexés ; Commandes DROP TABLE.
Il recommande également d'associer la paramétrisation à une validation minimale des entrées : assainir ou bloquer les caractères improbables dans un nom de famille (par exemple, les points-virgules ou les marqueurs de commentaires --) tout en autorisant les caractères légitimes tels que les apostrophes (O'Reilly). Les requêtes paramétrées + le nettoyage des entrées offrent une protection substantielle contre les attaques par injection SQL.
Réparation 2 - Procédures stockées
Tim next montre deux procédures stockées : une procédure stockée non sécurisée qui concatène le code SQL à l'intérieur de la proc et l'exécute ensuite, et une procédure stockée sécurisée qui utilise directement les paramètres.
-
Une procédure stockée non sécurisée construit une chaîne @sql à partir du paramètre et l'exécute - toujours vulnérable à l'injection.
- La procédure stockée sécurisée exécute SELECT ... WHERE LastName = @LastName et s'exécute avec le paramètre - safe.
Tim précise que les procédures stockées ne sont pas un remède automatique si elles contiennent encore du code SQL dynamique. Mais lorsqu'elles sont utilisées correctement (pas de SQL dynamique), les procédures stockées permettent de centraliser les instructions SQL, ce qui facilite le paramétrage et l'audit des requêtes. Les procédures stockées peuvent également simplifier la prévention des injections SQL dans votre application.
Ne vous fiez à aucune donnée, pas même à celles des bases de données
Tim soulève un point important, souvent négligé : il ne faut pas faire aveuglément confiance aux données extraites de sa propre base de données SQL. Les attaquants placent parfois des charges utiles malveillantes dans des colonnes (une "bombe à retardement") qui seront ensuite concaténées en SQL dynamique par un autre processus. Tim insiste sur le fait qu'il faut toujours utiliser des paramètres et assainir les données à chaque étape - qu'elles proviennent d'un formulaire web, d'un téléchargement de fichier ou de votre propre base de données - afin que les données malveillantes ne puissent pas devenir un moyen d'injection.
Conseil bonus - moindre privilège et limitation des privilèges de la base de données
Au-delà des corrections de code, Tim recommande une configuration défensive : limitez les privilèges de base de données pour vos comptes d'application. Dans sa démo, la connexion utilise un compte administrateur via la sécurité intégrée - dangereux. Utilisez plutôt le principe du moindre privilège :
-
Créez un compte de base de données pour l'application avec uniquement les droits dont elle a besoin.
-
Si vous utilisez des procédures stockées, accordez à ce compte uniquement l'autorisation EXECUTE sur des procédures stockées spécifiques, et rien d'autre.
- N'accordez pas aux comptes d'application de larges privilèges d'administrateur permettant de DROP TABLE, de répertorier toutes les tables ou de lire d'autres bases de données.
Cela réduit l'impact d'une attaque réussie par injection SQL - même si l'injection est possible, l'attaquant ne peut pas faire plus que ce que le compte est autorisé à faire.
Tim fait également remarquer qu'Entity Framework complique les choses : EF nécessite souvent des autorisations élevées (migrations, modifications de schémas). Si vous utilisez EF en production, planifiez soigneusement ses autorisations et son déploiement.
Recap - arrêter, paramétrer, assainir, limiter
Tim conclut sa vidéo par une liste de contrôle claire pour prévenir les injections SQL dans les applications C# :
-
Ne construisez plus d'instructions SQL avec concaténation de chaînes ou SQL dynamique qui inclut les données de l'utilisateur.
-
Utilisez des requêtes paramétrées / des modèles d'instructions préparées pour que les données des utilisateurs soient toujours traitées comme des données.
-
Assainissez les entrées le cas échéant (points-virgules en bloc, commentaires SQL, caractères inattendus).
-
Préférez les procédures stockées sûres (pas de SQL dynamique à l'intérieur) pour centraliser la logique des requêtes.
-
Appliquer le principe du moindre privilège aux comptes de base de données - limiter ce que l'utilisateur de la base de données de l'application peut faire.
- Examiner le code (en particulier les endroits où le langage SQL est construit de manière dynamique) et tester les failles d'injection SQL.
Dernier avertissement de Tim : une gestion négligente des entrées utilisateur, du SQL dynamique et des privilèges excessifs des bases de données peut entraîner de graves violations - fuites de données sensibles, destruction de tables ou exfiltration non détectée sur le long terme. La prévention des injections SQL doit être considérée comme une exigence de sécurité fondamentale, et non comme une option.
