Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

CoqBlog

.NET is good :-)
{ Blog de coq }

Actualités

L'injection SQL n'est PAS un problème QUE pour les développeurs web !

J'ai l'impression que pas mal de personnes sont parties sur une fausse idée avec ce problème d'injection SQL : certains ont l'air de penser qu'il s'agit uniquement d'un problème rencontré avec les applications dotées d'une interface utilisateur web (dans l'écosystème qui nous intéresse : ASP.NET, et toute les technologies reposant dessus).

 

 

L'injection SQL est uniquement un problème de développeur Web ?

Ce n'est absolument pas le cas : une application WinForm/WPF/Console/... sera impactée elle aussi.
Certes une application web offre probablement un risque d'exploitation de la faille plus élevé de par son exposition à un plus grand nombre de sources d'attaques, et certainement un nombre de points d'entrées plus important (zones de saisie, querystring, cookies, etc), mais il n'en demeure pas moins que l'utilisateur agissant de l'intérieur n'est pas plus digne de confiance qu'un anonyme sur le réseau (de l'entreprise ou internet).

 

 

Mais qu'est ce que l'injection SQL ?

L'attaque par injection SQL est une attaque reposant sur une faille de sécurité (défaut de vérification et sécurisation des entrées) dont le but à parvenir à provoquer l'exécution d'un code malicieux, initialement non prévu par le système vulnérable.
Pour cela, on procède tout simplement par insertion de ce code dans des chaînes de caractères qui seront par la suite utilisées pour bâtir un ordre SQL envoyé au serveur SQL pour exécution, en s'arrangeant pour qu'au final l'insertion dans une requête SQL rende notre code exécutable. Le serveur SQL n'a aucune raison valable de ne pas l'exécuter à partir du moment où il est valide.

Il se présente donc notamment si vous utilisez certains types d'informations pour les insérer dans des requêtes SQL sans prendre de précautions particulières :

  • saisies par l'utilisateur dans un formulaire (quelle que soit la technologie utilisée, pas forcément web)
  • provenant de champs cachés dans le formulaire
  • provenant de paramètre d'url (querystring)
  • provenant de cookies
  • ...

Mais recadrons les choses différemment : le problème d'injection SQL n'est pas seulement lié à un type d'interface de saisie, il n'est même pas lié seulement au fait que la donnée est saisie : il est lié à la donnée en elle même.
Le souci peut très bien se présenter dans un processus sans intervention humaine directe, avec par exemple traitement de données issues de fichier CSV/XML/... provenant de sources diverses. Ces données ont donc potentiellement été traitées par un être humain à un lointain bout de la chaîne, directement ou pas : le texte manipulé peut provenir d'une opération d'OCR par exemple.
Un autre point important à garder à l'esprit est que l'attaque par injection SQL n'est pas forcément à effet direct lorsque l'utilisateur saisi son code "malicieux" au travers du moyen approprié : ses effets peuvent être déclenchés durant toute la durée de vie de la donnée.

 

 

Seulement un problème de sécurité ?

Hormis le côté sécurité (vol, destruction, etc) du problème, il y a un autre aspect de la chose identique en tout point hormis le côté volontaire qui caractérise l'attaque par injection SQL : la corruption involontaire de l'ordre SQL. Cet aspect là, tout le monde doit le connaitre.
Certes il ne s'agit pas directement à proprement parler d'injection SQL vu qu'il ne s'agit pas réellement d'une attaque mais le fond est le même, et l'existence de ce problème de corruption rend l'attaque par injection SQL possible.

Un exemple courant est celui tout simple de la gestion de personnes : vous enregistrez des noms et prénoms, avec un code de ce genre pour la production du code SQL :

// NE PAS UTILISER CECI ! / DON'T USE THAT !
String query = String.Format(CultureInfo.InvariantCulture,
  "INSERT INTO [MonSchema].[MaTable] ([FirstName], [LastName]) VALUES ('{0}', '{1}');"
,
 
firstName,
 
lastName
 
);

Pas de chance, un beau jour vous devez enregistrer les informations d'une personne dont le nom comporte une apostrophe, et la requête générée a alors cet aspect là :

INSERT INTO [MonSchema].[MaTable] ([FirstName], [LastName]) VALUES ('Jean', 'D'upont');

Dans le meilleur des cas nous avons un ordre invalide et une erreur d'exécution, causant probablement des blocages, pertes financières, etc le temps de corriger le code mais peut être pas de corruption/destruction de données.
Par contre dans le pire des cas nous avons involontairement un code exécutable par le serveur différent de celui que nous avions prévu, qui ne sera peut être pas détecté dans l'immédiat si les effets produits ne sont pas flagrants, mais qui peut donc présenter un fort risque de corruption/perte de données.
Imaginez ici que l'opération d'OCR d'où provient le texte à persister porte sur un livre parlant de SQL, avec un exemple de code montrant comment supprimer les enregistrements de toutes les tables de la base courante...

Pour la suite de ce post, je considèrerais que les deux aspects du problème ne font qu'un, d'ailleurs les solutions pour l'un empêchent l'autre de se produire.
Les exemples sont quant à eux basés sur .NET (en C#) et SQL Server, mais le problème ne touche bien évidemment pas que ces technologies là.

 

 

Les solutions ?

Comment palier à ce problème ?
"Nettoyer" soit même les entrées est illusoire : vous ne connaissez probablement pas toutes les subtilités des différents moteurs de base de données, et ces moteurs sont de toute façon amenés à évoluer. "Nous modifierons le code à ce moment là" n'est pas une réponse valide : le code risque de mal vieillir.

Attention, soyons clair, je parlais bien ici de nettoyage en vue d'éviter la corruption de l'ordre SQL, pas de la nécessaire validation des entrées qui n'est pas directement attachée au problème dont nous parlons.
Il s'agit par exemple de la vérification des tailles, longueurs : si vous offrez une zone "commentaires", il y a des chances que vous ayez besoin pour elle de plus de 4000 caractères, auquel cas elle sera sans doute persistée en base sous forme d'un type nvarchar(MAX). Mais ça ne veut pas pour autant dire que vous voulez que la personne puisse envoyer 2Go de texte en base.

 

Les solutions en général proposées sont :

  • utiliser des requêtes paramétrées
  • utiliser des procédures stockées

Nous n'entrerons pas ici dans le débat de fond pour ou contre l'utilisation de procédures stockées, ce n'est pas le sujet.
A la liste précédente, nous pouvons ajouter : utiliser un outil de mapping objet/relationnel. Mais nous nous assurerons que le code SQL qu'il génère est bien évidemment paramétré et non pas basé sur de bêtes concaténations. Il ne s'agit pas de déporter le problème loin de nos yeux, mais d'ajouter une chance supplémentaire que les développeurs finaux ne fassent pas d'erreurs.
Cet outil va donc au final reposer sur une des solutions proposées, et donc est plus une couche supplémentaire qu'une solution directe. Sur le sujet qui nous concerne au travers de ce post, il aura surtout l'avantage de permettre aux architectes de limiter encore plus les risques de dérapage de la part des personnes qui exécutent le travail.

Est ce que ces 2 solutions se suffisent à elles mêmes ? Est ce que le simple fait de les utiliser suffit pour garantir la sécurité des données ? Non, il faut réellement que les personnes qui vont intervenir sur l'accès aux données comprennent ce problème.

Concernant les requêtes paramétrées, l'élément le plus proche de la requête dynamique habituelle (et dangereuse), et donc le plus simple à mettre en oeuvre à la place de cette dernière, il n'y a pas (à ma connaissance) grand chose de plus à faire pour sécuriser un peu plus.
Si la requête effectue un ordre INSERT, il faut que l'utilisateur ait directement ce droit sur les objets cibles.
Concernant les procédures stockées, il y a plus à dire. Le simple fait de déporter l'ordre INSERT dans une procédure stockée ne vous permet pas de passer magiquement d'un risque majeur à un risque zéro : encore faut t'il que l'appel de la procédure soit effectué de façon... paramétrée. En effet au final l'utilisation de procédures stockées est plus une couche supplémentaire qu'une solution directe au problème.

Rappelons qu'il y a au moins 2 moyens d'exécuter une procédure stockée depuis du code .NET : utiliser directement les facilités offertes par les objets d'accès aux données au travers de IDbCommand.CommandType en lui affectant CommandType.StoredProcedure, ou utiliser l'ordre EXECUTE dans un requête tout ce qu'il y a de plus commun.
On a tendance à oublier ce second moyen, mais le danger est pourtant bien à ce niveau là.
En utilisant CommandType.StoredProcedure, les données seront forcément spécifiée via l'implémentation de IDataParameter spécifique au provider utilisé, alors qu'au contraire avec l'utilisation de l'ordre EXECUTE vous avez le risque qu'un de vos développeurs écrive quelque chose de ce genre :

// NE PAS UTILISER CECI ! / DON'T USE THAT ! 
String query = String.Format(CultureInfo.InvariantCulture,
  "EXECUTE [MonSchema].[AddPerson] @FirstName='{0}', @LastName='{1}';"
,
 
firstName,
 
lastName
 
);

Du coup, vous avez ici un formidable exemple de fausse impression de sécurité : la personne a utiliser une procédure stockée, c'est donc sécurisé. Ce n'est bien sûr pas du tout le cas, la requête ayant cet aspect là pour notre ami Jean D'upont :

EXECUTE [MonSchema].[AddPerson] @FirstName='Jean', @LastName='D'upont';

Placez maintenant un ordre SQL là où il faut...
Vous devez donc bien faire attention à la façon dont sont compris les conseils que vous donnez.

 

C'est là qu'on arrive sur un autre aspect à prendre en compte lorsque l'on a opté pour l'utilisation exclusive de procédures stockées : la seule permission dont a réellement besoin l'identité utilisée (qui n'a bien entendu pas reçu le rôle db_owner, n'est ce pas) par l'application cliente pour l'accès à la base de données est EXEC sur ces fameuses procédures et rien d'autre, limitant ainsi les impacts d'une éventuelle attaque réussie.
C'est là que vous aurez besoin de dialoguer un peu avec votre DBA préféré, il doit aimer jouer avec ces choses là.

Il s'agit ici d'une autre "solution" directement couplée à l'utilisation de procédures stockées qu'on voit parfois abordée quand on parle de ce problème d'injection SQL : utiliser des permissions en exécution seule.
Je ne l'ai pas citée plus haut car ce n'en est pas réellement une. En effet elle ne permet en rien de résoudre directement le problème mais elle vient plutôt en complément et permet en partie de limiter les impacts d'une attaque réussie.

Attention, comprenons nous bien : la limitation des droits de l'utilisateur à de simples permissions en exécution ne vous permettrons pas pour autant de faire l'appel de la procédure stockée n'importe comment en tout sécurité.
En reprenant notre exemple précédent, si le code SQL injecté par l'assaillant est un ordre INSERT/UPDATE/etc sur une table, il échouera. Mais s'il s'agit encore de notre WHILE, il sera exécuté, provoquant une consommation excessive de ressources.
L'attaquant pourra aussi se reposer sur l'appel d'autres procédures stockées auxquelles à accès l'utilisateur, qui lui permettront peut être d'altérer/détruire les données.
Il se peut aussi qu'au travers d'une attaque il puisse accéder à un serveur lié et que cette liaison aie été effectuée, pour diverses raisons, avec des credentials possédant un niveau de privilèges plus élevé.
Sans parler des manipulations qui pourraient permettre d'arriver à une élévation de privilèges.

De manière générale limiter les privilèges de l'utilisateur au strict nécessaire n'est jamais une mauvaise chose (par exemple empêcher l'utilisation de choses comme xp_cmdshell, Database Mail, ... si l'utilisateur n'a aucune raison valable d'y avoir accès), mais vous ne pouvez pas considérer cette seule action comme suffisante.

 

 

J'ai vérifié le code traitant des données externes, je peux me reposer sur mes lauriers maintenant ?

Absolument pas !
Souvenez vous, j'ai dit plus haut que l'attaque n'était pas forcément à effet direct : si vous avez fait en sorte qu'un code malicieux saisi ne soit pas exécuté lors de l'enregistrement des données en base, vous ne pouvez pas arrêter oublier l'injection SQL et penser que vos données sont maintenant dénuées de tout risque.

Imaginez que votre utilisateur s'est identifié en tant que "Jean" / "Dupont'); WHILE 1=1 DECLARE @nb int; --".
Grâce à votre enregistrement des données avec une requête paramétrée, il dispose maintenant d'un nom assez ridicule dans votre application. Mais justement, son nom est bel et bien "Dupont'); WHILE 1=1 DECLARE @nb int; --" en base, sans aucune conséquence réelle pour l'instant vu que c'est une donnée.

Mais que se passe t'il si vous partez du principe qu'une fois en base vos données sont saines et que fort de ce sentiment vous utilisez une concaténation de chaînes pour bâtir un ordre au lieu de faire encore et toujours une requête paramétrée ?
Dans notre cas le "WHILE 1=1 DECLARE @nb int;" devient exécutable, vous aimez les boucles infinies ? (oui, il y a des timeouts, mais tout de même).

La règle est toujours aussi simple : si vous devez utiliser des données provenant de votre base pour bâtir d'autres requêtes, utilisez des paramètres / procédures stockées (correctement appelées). Et de toute façon, comme dit plus haut, vous n'êtes toujours pas à l'abri d'un apostrophe légitime, donc pourquoi prendre ce risque d'obtenir un ordre SQL invalide même si les données stockées sont dignes de confiance (si ça arrive réellement...) ? Et repensez aussi au coup de l'OCR...

 

 

Et dans le code des procédures stockées ? (ou le restant du batch d'une requête paramétrée)

Voilà un dernier point auquel on ne pense pas forcément, et pourtant le risque est bien là : vous ne devez pas faire n'importe quoi non plus dans le code SQL utilisant les paramètres, que ce soit un simple batch ou une procédure stockée, fonction, ...
La simple utilisation de procédures stockées (et des paramètres en général) ne vous garanti pas que votre donnée est définitivement saine, ça vous garanti juste que la donnée sera transmise en tant que tel.

Pour illustrer, prenons l'exemple d'une procédure stockée permettant de faire une recherche des personnes dont le nom commence par celui d'une autre, et que pour une raison valable (dans l'exemple courant il n'y en a pas réellement) vous effectuez cette recherche au moyen d'une requête dynamique définie dans le corps de la procédure :

-- NE PAS UTILISER CECI ! / DON'T USE THAT 
CREATE PROCEDURE [MonSchema].[FindPersonBAD] 
( 
   
@LastName  nvarchar(256) 
) 
AS 
BEGIN 
    
   
-- NE PAS UTILISER CECI ! / DON'T USE THAT 
    DECLARE @sql nvarchar(4000);
    
   
SET @sql = N'SELECT [FirstName], [LastName] 
        FROM [MonSchema].[MaTable] 
        WHERE [LastName] LIKE '''
+ @LastName + N'%'''; 
    
   
EXECUTE (@sql); 
   
-- NE PAS UTILISER CECI ! / DON'T USE THAT 

END 

Si jamais un agresseur a prévu ce genre de cas, et que son nom est "Dupont%'; WHILE 1=1 DECLARE @nb int; --", vous venez une nouvelle fois de revivre le coup de la boucle infinie (certes vos DBA, si DBA il y a, ont probablement limités les effets de ce code précis en limitant la durée maximum d'exécution des requêtes mais quand même...).
Si ça ne vous suffit pas, imaginez un remplacement de la boucle par un code plus "sympathique", comme un ordre UPDATE : corruption d'informations et impact sur les performances en cas de table très volumineuse.

Si vous devez vraiment utiliser du SQL dynamique dans votre code SQL, ayez au moins le réflexe de passer par des paramètres : la procédure sp_executesql vous permet d'y arriver très simplement :

CREATE PROCEDURE [MonSchema].[FindPerson] 
( 
   
@LastName  nvarchar(256) 
) 
AS 
BEGIN 
    
   
-- TODO : valider les entrées
    
   
DECLARE @sql nvarchar(4000);
    
   
SET @sql = N'SELECT [FirstName], [LastName] 
        FROM [MonSchema].[MaTable] 
        WHERE [LastName] LIKE @Name+''%'''
; 
    
   
EXECUTE sp_executesql 
       
@stmt = @sql, 
       
@params = N'@Name nvarchar(256)', 
       
@Name = @LastName; 

END

Bien sûr, dans l'hypothèse que le SQL dynamique soit réellement nécessaire : dans le cas contraire, passez vous en.

 

 

Et là où on ne peut vraiment pas utiliser de paramètres ?

Il peut se présenter des cas pour lesquels utiliser un paramètre directement dans la requête n'est réellement pas possible, comme par exemple avec une clause TOP avec des versions de SQL Server inférieures à SQL Server 2005.

Dans ce cas vous devrez vous même assurer la sécurité, et donc prendre les mesures qui s'imposent : validation des types, validation des longueurs, ...
Dans le cas présent, il y a de fortes chances que votre valeur aie été passée à la procédure stockée sous forme d'un paramètre type int/bigint/float, mais n'oubliez pas de valider la plage de valeurs possible : si dans votre esprit la taille des pages affichées par votre application peut aller de 10 à 100 lignes, un attaquant aura peut être l'envie d'en demander quelques millions...

Si c'est du SQL dynamique généré côté client, n'insérez pas directement la valeur de filtrage à partir d'une chaîne : passez par Int32/Int64/Double (via TryParse si disponible), ça vous permettra de valider le type et la plage assez facilement.

Si la donnée est typée texte, ne cédez pas pour autant à la fatalité : par exemple s'il s'agit de manipuler des identifiants d'objets, vérifiez que la valeur qui vous a été spécifiée est bien celle d'un objet existant (voir OBJECT_ID, OBJECT_NAME, OBJECT_SCHEMA_NAME, etc)

 

Bonne revue de code.

Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :
Posted: samedi 5 juillet 2008 01:08 par coq

Commentaires

aemond a dit :

Article très complet... Bravo !

# juillet 7, 2008 12:14

coq a dit :

Merci :-)

# juillet 11, 2008 20:32

coq a dit :

Au sujet de la validation des entrées dans un contexte d'application ASP.NET, voir le post de Cyril : ASP.net - tout savoir sur la validation des entrées utilisateurs | les controles de validation

http://blogs.codes-sources.com/cyril/archive/2008/09/02/asp-net-tout-savoir-sur-la-validation-des-entr-es-utilisateurs-les-controles-de-validation.aspx

# septembre 6, 2008 10:12
Les commentaires anonymes sont désactivés

Les 10 derniers blogs postés

- Merci par Blog de Jérémy Jeanson le 10-01-2019, 20:47

- Office 365: Script PowerShell pour auditer l’usage des Office Groups de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 11:02

- Office 365: Script PowerShell pour auditer l’usage de Microsoft Teams de votre tenant par Blog Technique de Romelard Fabrice le 04-26-2019, 10:39

- Office 365: Script PowerShell pour auditer l’usage de OneDrive for Business de votre tenant par Blog Technique de Romelard Fabrice le 04-25-2019, 15:13

- Office 365: Script PowerShell pour auditer l’usage de SharePoint Online de votre tenant par Blog Technique de Romelard Fabrice le 02-27-2019, 13:39

- Office 365: Script PowerShell pour auditer l’usage d’Exchange Online de votre tenant par Blog Technique de Romelard Fabrice le 02-25-2019, 15:07

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Stream Portal par Blog Technique de Romelard Fabrice le 02-21-2019, 17:56

- Office 365: Script PowerShell pour auditer le contenu de son Office 365 Video Portal par Blog Technique de Romelard Fabrice le 02-18-2019, 18:56

- Office 365: Script PowerShell pour extraire les Audit Log basés sur des filtres fournis par Blog Technique de Romelard Fabrice le 01-28-2019, 16:13

- SharePoint Online: Script PowerShell pour désactiver l’Option IRM des sites SPO non autorisés par Blog Technique de Romelard Fabrice le 12-14-2018, 13:01