Avant-propos▲
Au cours de cet article, nous allons étudier l’utilisation de requêtes paramétrées dans le cadre d’une application ASP.Net codée en C#, il est évident que cela peut être étendu aux autres types d’applications et langages .Net moyennant quelques petites adaptations mineures. Les requêtes paramétrées avec SqlParameter (ce qui est ici le cas) fonctionnent avec SQL Server, mais aussi avec MSDE.
A. Introduction▲
Une vulnérabilité des applications ASP.Net utilisant une base de données est le fait que l’utilisateur puisse exécuter des commandes SQL alors que théoriquement cela devrait lui être impossible. Ce type d’attaque peut potentiellement permettre à un utilisateur de compromettre l’intégrité des données et de la base elle-même. Cette vulnérabilité est connue sous le nom d’injection SQL. Ce problème de sécurité se pose, par exemple, lorsqu’un utilisateur remplit des champs d’un formulaire dont le contenu sera concaténé directement à l’intérieur des requêtes envoyées à la base de données.
B. Les attaques de type injection SQL▲
Pour vous montrer l’intérêt d’employer des requêtes SQL paramétrées, il est utile de voir à quel point la sécurité des données peut être compromise si l’on n’applique pas quelques principes simples.
Prenons, pour cela, un exemple concret. Imaginons que votre application contienne un formulaire permettant de faire une recherche dans votre base de données en retournant les enregistrements correspondants à des critères établis par l’utilisateur. De plus, admettons qu’un champ permette à l’utilisateur d’entrer un texte au clavier pour déclarer à l’application ce qu’il recherche. L’utilisateur normal va taper les mots clés en relation avec ce qu’il recherche, mais là où il y a danger, c’est qu’un utilisateur malveillant va non pas taper de simples mots clés, mais des instructions SQL dans le but de modifier ou détruire vos données.
En effet, sans l’emploi de requêtes paramétrées, il y a de grandes chances que le code de la requête dans votre application soit :
Select
*
From
maTable Where
monChamp =
'les_mots_clés_entrés_par_l’utilisateur'
;
Pour l’utilisateur malveillant souhaitant effacer des données c’est alors un jeu d’enfant… Dans le champ de votre formulaire, il va par exemple saisir des commandes SQL, ou encore employer des apostrophes, ou même des points virgules permettant ainsi d’ajouter de nouvelles instructions à la première requête transmise par l’application. Voici concrètement à quoi va ressembler dans le code une telle attaque :
maRequete =
"SELECT nom, prenom FROM maTable Where identifiant = '"
+
maTextBox.
Text +
"'"
;
L’utilisateur malveillant va alors saisir dans maTextBox :
15' ; DELETE FROM maTable WHERE identifiant <>'';
La requête complète, dans votre application, va alors être :
maRequete =
"SELECT nom, prenom FROM maTable Where identifiant = '15' ; DELETE FROM maTable WHERE identifiant <>'';"
;
Cela va avoir pour conséquence directe d’effacer tous les enregistrements de votre table… Cela bien entendu si le compte utilisé pour la requête n’est pas autorisé à faire seulement des SELECT sur la table concernée, cela est malheureusement très souvent le cas pour des raisons de commodité et de « qui peut le plus peut le moins »… ainsi l’on se débarrasse vite fait d’une gestion fine et efficace des droits ; en utilisant tout le temps le compte SA pour faire de simples SELECT…
Bien entendu, cela suppose également que l’utilisateur malveillant connaisse un minimum la structure de votre base de données, mais il ne faut absolument pas sous-estimer ce risque, surtout que tout le monde a toujours tendance à avoir des structures similaires.
J’ai pris un exemple volontairement simpliste, mais il est clair que vu la richesse des différentes versions du langage SQL employées avec les bases de données actuelles les attaques peuvent être beaucoup plus subtiles.
Vous comprenez, désormais, à quel point votre application peut être en danger face une attaque de type SQL Injection. Il faut donc proscrire la concaténation de chaînes dans les requêtes SQL de vos applications ASP.Net et n’employer que des requêtes paramétrées ou des procédures stockées.
C. Les requêtes paramétrées▲
Une requête paramétrée est, en fait, une commande SQL comme les autres sauf qu’on lui passe une collection de paramètres dont le type et la taille ont été définis. Les paramètres dans la requête sont nommés avec un @ comme premier caractère.
SELECT
nom FROM
Clients WHERE
Nom =
@nomClient
Il faut aussi, préalablement à l’exécution de la requête, définir la taille et le type des paramètres, mais nous verrons cela un peu plus tard.
Attention : toutes les commandes T-SQL ne fonctionneront pas avec un paramètre, par exemple « Use @ma_base » va soulever une exception, il faut absolument que le paramètre soit utilisé dans une expression (par exemple xxx = @param ou zzz * @param). Ceci n’est embêtant que dans quelques situations, et ne remet pas en question l’utilité des requêtes paramétrées.
D. Les classes du .Net framework▲
Les classes qui sont concernées sont les suivantes :
SqlParameter est la classe qui permet de gérer ces paramètres, sa méthode la plus utilisée est Add, en effet c’est elle qui permet d’ajouter les paramètres à notre requête.
Entre en jeu aussi les classes SqlCommand, SqlConnection qui permettent de définir des commandes T-SQL et de se connecter à SQL Server, plus largement les classes concernées sont celles de l’espace de noms System.Data.SqlClient. Cet espace de noms est le fournisseur de données .Net pour Microsoft SQL Server®.
Remarque : pour utiliser toutes ces fonctionnalités, il faut importer l’espace de noms cité précédemment en ajoutant la ligne de code Imports System.Data.SqlClient, cet espace contient la classe SqlParameter qui nous intéresse ici.
E. Les requêtes paramétrées dans le code▲
L’exemple, ci-dessous, vous montre ce qu’est une requête paramétrée, elle permet d’insérer des données dans une base de données
SqlConnection maConnexionSQLsrv;
SqlCommand myCommand;
string
maRequete,
ConnexionSQL;
//Récupération de la chaine de connexion au serveur de base de données
ConnexionSQL =
ConfigurationSettings.
AppSettings [
"ConnectionStringSQLsrv"
];
maConnexionSQLsrv =
new
SqlConnection
(
ConnexionSQL);
//Requete mise a jour de la table client
maRequete =
"INSERT INTO [article] ( [idclient],[idarticle],[prixarticle])
VALUES (
@id_article,
@prix_article,
@nom_article)";
//Création de la commande SQL
myCommand =
new
SqlCommand (
maRequete,
maConnexionSQLsrv);
//Création et décalartion des paramètres
myCommand.
Parameters.
Add (
new
SqlParameter
(
"@id_article"
,
SqlDbType.
Int,
4
));
myCommand.
Parameters.
Add (
new
SqlParameter
(
"@prix_article"
,
SqlDbType.
Float,
8
));
myCommand.
Parameters.
Add (
new
SqlParameter
(
"@nom_article"
,
SqlDbType.
VarChar,
25
));
//Attribution des valeurs aux paramètres
myCommand.
Parameters [
"@id_article"
].
Value =
Convert.
ToInt32
(
maTextBox1.
Text);
myCommand.
Parameters [
"@prix_article"
].
Value =
Convert.
ToDouble
(
maTextBox2.
Text);
myCommand.
Parameters [
"@nom_article"
].
Value =
maTextBox3.
Text;
try
{
//Execution de la requête
myCommand.
Connection.
Open (
);
myCommand.
ExecuteNonQuery
(
);
myCommand.
Connection.
Close
(
);
}
catch
(
SqlException ex)
{
Response.
Write
(
ex.
Message);
}
Maintenant, il ne suffit pas d’avoir ce code et de le copier, il faut surtout le comprendre. Il n’est pas nécessaire d’expliquer les premières lignes en effet elles sont exactement les mêmes que pour une requête non paramétrée. Là où cela diffère, c’est lorsque l’on déclare les paramètres.
Les paramètres sont, en fait, ajoutés à myCommand qui est un objet sqlCommand, c’est-à-dire une chaîne d’instructions en Transact-SQL qui doit être exécutée par SQL Server. Pour ajouter un paramètre à un objet sqlCommand, rien de plus simple, il suffit d’ajouter le code suivant :
maCommande.
Parameters.
Add (
new
SqlParameter
(
"@mon_paramètre"
,
le_type_du_paramètre));
Si notre paramètre se nomme param1 et qu’il est de type Integer voilà ce que va donner notre ligne :
maCommande.
Parameters.
Add (
new
SqlParameter
(
"@param1"
,
SqlDbType.
Int,
4
));
Les paramètres peuvent être de types différents, voilà une liste exhaustive des types existants :
Type |
Description |
---|---|
BigInt |
Entier signé 64 bits. |
Binary |
Array de type Byte. Flux de données binaires de longueur fixe comptant entre 1 et 8 000 octets. |
Bit |
Valeur numérique non signée pouvant être égale à 0, 1 ou une référence null (Nothing dans Visual Basic). |
Char |
Flux de caractères non Unicode de longueur fixe comptant entre 1 et 8 000 caractères. |
DateTime |
Données de date et d’heure dont la valeur est comprise entre le 1er janvier 1753 et le 31 décembre 9999, avec une précision de 3,33 millisecondes. |
Decimal |
Valeur numérique fixe de précision et d’échelle comprise entre -10^38 -1 et 10^38 -1. |
Float |
Nombre en virgule flottante compris entre -1,79E +308 et 1,79E +308. |
Image |
Array de type Byte. Flux de données binaires de longueur variable comptant entre 0 et 2 31 -1 (ou 2 147 483 647) octets. |
Int |
Entier signé 32 bits. |
Money |
Valeur monétaire comprise entre -2 63 (ou -922 337 203 685 477,5808) et 2 63 -1 (ou +922 337 203 685 477,5807), avec une précision d’un dix millième d’unité monétaire. |
Nchar |
Flux de caractères Unicode de longueur fixe comptant entre 1 et 4 000 caractères. |
Ntext |
Flux de données Unicode de longueur variable dont la longueur maximale est égale à 2 30 - 1 (ou 1 073 741 823) caractères. |
NvarChar |
Flux de caractères Unicode de longueur variable comptant entre 1 et 4 000 caractères. Remarque : la conversion implicite échoue si une chaîne compte plus de 4 000 caractères. Définissez l’objet de manière explicite lorsque vous utilisez des chaînes comptant plus de 4 000 caractères. |
Real |
Nombre en virgule flottante compris entre -3,40E +38 et 3,40E +38. |
SmallDateTime |
Données de date et d’heure dont la valeur est comprise entre le 1er janvier 1900 et le 6 juin 2079, avec une précision d’une minute. |
SmallInt |
Entier signé 16 bits. |
SmallMoney |
Valeur monétaire comprise entre -214 748,3648 et +214 748, 3647, avec une précision d’un dix millième d’unité monétaire. |
Text |
Flux de données non Unicode de longueur variable dont la longueur maximale est égale à 2 31 -1 (ou 2 147 483 647) caractères. |
Timestamp |
Array de type Byte. Nombres binaires générés automatiquement et garantis comme étant uniques dans une base de données. timestamp sert généralement de mécanisme d’insertion d’informations de version dans les lignes de tables. La taille de stockage est égale à 8 octets. |
TinyInt |
Entier non signé 8 bits. |
UniqueIdentifier |
GUID (Identificateur global unique). |
VarBinary |
Array de type Byte. Flux de données binaires de longueur variable comptant entre 1 et 8 000 octets. Remarque : la conversion implicite échoue si un tableau d’octets compte plus de 8 000 octets. Définissez l’objet de manière explicite lorsque vous utilisez des tableaux d’octets comptant plus de 8 000 octets. |
VarChar |
Flux de caractères non Unicode de longueur variable comptant entre 1 et 8 000 caractères. |
Variant |
Type de données spécial pouvant contenir des données numériques, de chaîne, binaires ou de date, ainsi que les valeurs Empty et Null SQL Server. Il s’agit du type supposé. |
Après avoir déterminé le nom et le type, il faut déterminer la taille. Pour les chaînes, la taille détermine le nombre de caractères, sinon elle détermine sa taille en octets. Comme nous pouvons le voir ci-dessous, notre chaîne VarChar pourra avoir une longueur maximale de 25 caractères.
myCommand.
Parameters..
Add
(
new
SqlParameter (
"@nom_article"
,
SqlDbType.
VarChar,
25
));
Ou encore ici notre entier sera codé sur 4 octets.
myCommand.
Parameters.
Add (
new
SqlParameter
(
"@id_article"
,
SqlDbType.
Int,
4
));
Maintenant que nous avons typé et dimensionné le paramètre, il faut lui attribuer une valeur, ici nous récupérons le texte contenu dans maTextBox1.
myCommand.
Parameters [
"@id_article"
].
Value =
Convert.
ToInt32
(
maTextBox1.
Text);
myCommand.
Parameters [
"@prix_article"
].
Value =
Convert.
ToDouble
(
maTextBox2.
Text);
myCommand.
Parameters [
"@nom_article"
].
Value =
maTextBox3.
Text;
Nous voyons que pour @id_article et @prix_article, il a fallu réaliser un cast sur ce que nous avons récupéré, en effet, nous récupérons deux chaînes de caractères que nous voulons transformer respectivement en données de type entier et double. Ce qui explique l’emploi de Convert.ToInt32 et Convert.ToDouble .
Et enfin, la dernière étape est tout simplement l’exécution de la requête précédée de l’ouverture de la connexion avec le serveur de base de données et suivie de sa fermeture.
myCommand.
Connection.
Open (
);
myCommand.
ExecuteNonQuery
(
);
myCommand.
Connection.
Close
(
);
F. Les avantages et inconvénients▲
Les avantages
- Sécurité : comme nous l’avons vu, c’est une excellente protection contre les attaques de type SQL Injection.
- Rapidité : une requête paramétrée est plus rapide dans la majorité des cas qu’une requête « standard », car les données sont compilées directement avec le code au lieu d’être ajoutées dans la base de données au dernier moment juste avant l’exécution de la requête.
- Lisibilité : une requête paramétrée est quand même beaucoup plus lisible qu’une requête avec des caractères de concaténation dans tous les sens, surtout quand celle-ci est complexe.
Les inconvénients
Même si le fait d’employer des requêtes paramétrées est un atout très important au niveau de la sécurité, il ne faut pourtant pas croire que cela est suffisant. Il est indispensable de configurer correctement sa base de données, avec des comptes ayant des accès en adéquation avec leurs besoins, par exemple ne pas utiliser le compte SA pour faire un SELECT sur une table (par ailleurs, il ne faut jamais utiliser le compte SA dans une application, sauf nécessité absolue).
De plus, même si nos paramètres sont typés, cela ne signifie pas qu’il y a un contrôle du type avant l’exécution de la requête, en clair si dans un champ devant contenir un entier, un utilisateur saisit 12,43, il va alors y avoir un plantage pur et simple. Il faut donc avoir une gestion des erreurs qui soit efficace avec pourquoi pas, une vérification côté client (si le navigateur le permet) et côté serveur des données. La validation de formulaire étant très bien implémentée dans le Framework .Net, ces opérations ne sont pas difficiles à réaliser et apportent sécurité et fiabilité à votre application.
Donc, pour résumer, il n’y a pas de réel inconvénient, le seul est que l’on pourrait croire que l’emploi de requêtes paramétrées est suffisant, mais attention cela n’est pas le cas.
G. Conclusion▲
L’emploi de requêtes paramétrées ne signifie bien évidemment pas qu’il n’est plus nécessaire de se préoccuper de la sécurité au niveau de la base de données elle-même ! Elle est toujours aussi importante comme nous venons de le voir juste précédemment. Mais cependant nous pouvons dire qu’au plan de la sécurité les requêtes paramétrées sont un atout important. La concaténation de chaînes dans les requêtes SQL est à proscrire dès que cela vous est possible.
Ressources▲
Documentation relative à la classe SQLparameter : Cliquez ici
Documentation relative à l’espace de nom System.Data.SqlClient : Cliquez ici
Un grand merci à Freegreg, Morpheus et Bidou pour leur aide.