Partager via


Injection de code SQL

S’applique à : SQL Server Azure SQL Database Azure SQL Managed Instance Azure Synapse Analytics Analytics Platform System (PDW)

Une injection SQL est une attaque dans laquelle un code malveillant est inséré dans les chaînes transmises ultérieurement à une instance du moteur de base de données SQL Server pour l’analyse et l’exécution. Les procédures de création d’instructions SQL doivent être vérifiées à la recherche d'éventuelles vulnérabilités d’injection, car le moteur de base de données exécute toutes les requêtes validées syntaxiquement qu'il reçoit. Même les données paramétrables peuvent être manipulées par un pirate compétent et déterminé.

Comment fonctionne l’injection SQL

Les injections SQL prennent principalement la forme d'insertions directes de code dans les variables d'entrée utilisateur qui sont concaténées avec des commandes SQL et exécutées. Des attaques par injection moins directes insèrent un code malveillant dans les chaînes destinées à être stockées dans une table ou en tant que métadonnées. Lorsque les chaînes stockées sont ensuite concaténées dans une commande SQL dynamique, le code malveillant est exécuté.

Le processus d'injection consiste à terminer prématurément une chaîne de texte et à ajouter une nouvelle commande. Étant donné que la commande insérée peut avoir d'autres chaînes ajoutées préalablement à son exécution, l'attaquant termine la chaîne injectée par une marque de commentaire --. Le texte qui vient à la suite est ignoré au moment de l'exécution.

Le script suivant montre un exemple d'injection SQL simple. Le script génère une requête SQL en concaténant des chaînes codées en dur avec une chaîne entrée par l’utilisateur :

var ShipCity;
ShipCity = Request.form ("ShipCity");
var sql = "select * from OrdersTable where ShipCity = '" + ShipCity + "'";

L'utilisateur est invité à entrer le nom d'une ville. Si l'utilisateur saisit Redmond, la requête assemblée par le script ressemble à ce qui suit :

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';

Mais, supposons que l'utilisateur saisit le texte suivant :

Redmond';drop table OrdersTable--

Dans ce cas, le script assemble la requête suivante

SELECT * FROM OrdersTable WHERE ShipCity = 'Redmond';drop table OrdersTable--'

Le point-virgule (;) indique la fin d'une requête et le début d'une autre. Le double tiret (--) signifie que le reste de la ligne courante est un commentaire et qu'elle doit être ignorée. Si le code modifié est syntaxiquement correct, il sera exécuté par le serveur. Lorsque le moteur de base de données exécute cette instruction, il sélectionne tous les enregistrements dans OrdersTable, où ShipCity est Redmond. Ensuite, le moteur de base de données exclut OrdersTable.

Tant que le code SQL injecté est syntaxiquement correct, il n'est pas possible de détecter cette manipulation par programme. Par conséquent, vous devez valider toutes les entrées utilisateur et vérifier très attentivement le code qui exécute les commandes SQL créées dans le serveur utilisé. Les meilleures pratiques de codage sont détaillées dans les sections suivantes de cette rubrique.

Validation de toutes les entrées

Validez toujours les entrées utilisateur en testant leur type, leur longueur, leur format et leur plage. Lorsque vous mettez en place les consignes à suivre contre les entrées malveillantes, tenez compte de l'architecture et des scénarios de déploiement de votre application. Rappelez-vous que les programmes conçus pour s'exécuter dans un environnement sécurisé peuvent être copiés dans un environnement non sécurisé. Les suggestions suivantes peuvent être considérées comme des pratiques recommandées :

  • Ne présumez pas de la taille, du type ou du contenu des données reçues par votre application. Par exemple, vous pouvez réaliser l'évaluation suivante :

    • Comment votre application réagira-t-elle si un utilisateur itinérant ou malveillant introduit un fichier vidéo de 2-GB, alors que votre application attend un code postal ?

    • Comment votre application réagira-t-elle si une instruction DROP TABLE est intégrée dans un champ de texte ?

  • Testez la taille et le type de données de l'entrée et appliquez les limites appropriées. Cela peut permettre d'éviter des dépassements de mémoire tampon délibérés.

  • Testez le contenu des variables de chaîne et acceptez uniquement les valeurs attendues. Rejetez les entrées contenant des données binaires, des séquences de caractères d'échappement et des caractères de commentaire. Cela peut empêcher l'injection de scripts et protéger contre l'utilisation de dépassements de mémoire tampon.

  • Lorsque vous utilisez des documents XML, validez toutes les données par rapport à leurs schémas, lorsqu'elles sont saisies.

  • Ne créez jamais d'instructions SQL transactionnelles directement à partir d'entrées utilisateur.

  • Utilisez des procédures stockées pour valider les entrées utilisateur.

  • Dans des environnements à plusieurs niveaux, toutes les données doivent être validées avant leur acceptation dans la zone de confiance. Les données qui ne passent pas le processus de validation doivent être rejetées et une erreur doit être renvoyée au niveau précédent.

  • Implémentez plusieurs niveaux de validation. Les précautions que vous prenez contre les utilisateurs malveillants occasionnels peuvent s'avérer inefficaces contre les attaquants obstinés. Une meilleure pratique consiste à valider les entrées dans l'interface utilisateur, puis à tous les points ultérieurs auxquels elles rencontrent une limite de confiance.

    Par exemple, la validation des données dans une application côté client peut empêcher l'injection de scripts simples. En revanche, si le niveau suivant considère que son entrée a déjà été validée, un utilisateur malveillant capable de contourner un client peut disposer d'un accès total à un système.

  • Ne concaténez jamais une entrée utilisateur qui n'a pas été validée. La concaténation de chaîne est le point d'entrée principal pour l'injection de scripts.

  • N’acceptez pas les chaînes suivantes dans des champs à partir desquels les noms de fichiers peuvent être créés : AUX, CLOCK$, COM1 jusqu’à COM8, CON, CONFIG$, LPT1 jusqu’à LPT8, NUL et PRN.

Si possible, rejetez les entrées qui contiennent les caractères suivants.

Caractère entré Signification dans Transact-SQL
; Délimiteur de requête
' Délimiteur de chaîne de données de caractères
-- Délimiteur de commentaire sur une seule ligne. Le texte suivant -- jusqu’à la fin de cette ligne n’est pas évalué par le serveur.
/*** ... ***/ Délimiteurs de commentaire. Le serveur n’évalue pas le texte qui figure entre /* et */.
xp_ Figure au début du nom des procédures stockées étendues de catalogue, telles que xp_cmdshell.

Utilisation de paramètres SQL de type sécurisé

La collection Parameters du moteur de base de données fournit le contrôle de type et la validation de longueur. Si vous utilisez la collection Parameters, les entrées sont traitées en tant que valeurs littérales et non pas en tant que code exécutable. Un autre avantage de la collection Parameters est qu’elle vous permet d’appliquer des contrôles de type et de longueur. Les valeurs situées en dehors de la plage déclenchent une exception. Le fragment de code suivant montre l’utilisation de la collection Parameters :

SqlDataAdapter myCommand = new SqlDataAdapter("AuthorLogin", conn);
myCommand.SelectCommand.CommandType = CommandType.StoredProcedure;
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
    SqlDbType.VarChar, 11);
parm.Value = Login.Text;

Dans cet exemple, le paramètre @au_id est traité en tant que valeur littérale et non pas en tant que code exécutable. Le type et la longueur de cette valeur font l'objet d'un contrôle. Si la valeur de @au_id n'est pas conforme aux contraintes de type et de longueur spécifiées, une exception est déclenchée.

Utilisation d'entrées paramétrables avec des procédures stockées

Les procédures stockées peuvent être la cible des injections SQL, si elles utilisent des entrées non filtrées. Par exemple, le code suivant est vulnérable :

SqlDataAdapter myCommand =
    new SqlDataAdapter("LoginStoredProcedure '" + Login.Text + "'", conn);

Si vous utilisez des procédures stockées, vous devez utiliser des paramètres en entrée.

Utilisation de la collection Paramètres avec des instructions SQL dynamiques

Si vous ne pouvez pas utiliser des procédures stockées, vous pouvez toujours utiliser des paramètres, comme le montre l’exemple de code suivant :

SqlDataAdapter myCommand = new SqlDataAdapter(
    "SELECT au_lname, au_fname FROM Authors WHERE au_id = @au_id", conn);
SqlParameter parm = myCommand.SelectCommand.Parameters.Add("@au_id",
    SqlDbType.VarChar, 11);
parm.Value = Login.Text;

Filtrer les entrées

Le filtrage des entrées peut également être utile pour protéger des risques d'injection SQL via la suppression des caractères d'échappement. Toutefois, en raison du grand nombre de caractères susceptibles de poser des problèmes, ce moyen de défense n'est pas fiable. L'exemple suivant permet de rechercher les délimiteurs de chaînes de caractères.

private string SafeSqlLiteral(string inputSQL)
{
    return inputSQL.Replace("'", "''");
}

Clauses COMME

Notez que si vous utilisez une clause LIKE, les caractères génériques devront quand même être séparés par des caractères d’échappement :

s = s.Replace("[", "[[]");
s = s.Replace("%", "[%]");
s = s.Replace("_", "[_]");

Examen du code à la recherche d'injections SQL

Vous devez examiner l’ensemble du code appelant EXECUTE, EXEC, ou sp_executesql. Vous pouvez utiliser des requêtes similaires à l'exemple suivant pour faciliter l'identification des procédures contenant ces instructions. Cette requête recherche les 1, 2, 3 ou 4 espaces après les mots EXECUTE ou EXEC.

SELECT object_Name(id)
FROM syscomments
WHERE UPPER(TEXT) LIKE '%EXECUTE (%'
    OR UPPER(TEXT) LIKE '%EXECUTE  (%'
    OR UPPER(TEXT) LIKE '%EXECUTE   (%'
    OR UPPER(TEXT) LIKE '%EXECUTE    (%'
    OR UPPER(TEXT) LIKE '%EXEC (%'
    OR UPPER(TEXT) LIKE '%EXEC  (%'
    OR UPPER(TEXT) LIKE '%EXEC   (%'
    OR UPPER(TEXT) LIKE '%EXEC    (%'
    OR UPPER(TEXT) LIKE '%SP_EXECUTESQL%';

Envelopper des paramètres avec QUOTENAME() et REPLACE()

Dans chaque procédure stockée sélectionnée, vérifiez que toutes les variables utilisées dans du Transact-SQL dynamique sont traitées correctement. Les données issues des paramètres d'entrée de la procédure stockée ou lues à partir d'une table doivent être enveloppées dans QUOTENAME() ou REPLACE(). Rappelez-vous que la valeur de @variable transmise à QUOTENAME() est de type sysname et sa longueur maximale est de 128 caractères.

@variable Wrapper recommandé
Nom d'un élément sécurisable QUOTENAME(@variable)
Chaîne de <= 128 caractères QUOTENAME(@variable, '''')
Chaîne de > 128 caractères REPLACE(@variable,'''', '''''')

Lorsque vous utilisez cette technique, une instruction SET peut être révisée comme suit :

-- Before:
SET @temp = N'SELECT * FROM authors WHERE au_lname ='''
    + @au_lname + N'''';

-- After:
SET @temp = N'SELECT * FROM authors WHERE au_lname = '''
    + REPLACE(@au_lname, '''', '''''') + N'''';

Injection activée par des données tronquées

Tout Transact-SQL dynamique affecté à une variable sera tronqué si sa taille dépasse la mémoire tampon attribuée à cette variable. Un attaquant capable de forcer la troncature d'une instruction en transmettant des chaînes inhabituellement longues à une procédure stockée peut manipuler le résultat. Par exemple, l’exemple suivant de procédure stockée est vulnérable face aux injections autorisées par la troncature.

Dans cet exemple, nous avons une mémoire tampon @command avec une longueur maximale de 200 caractères. Nous avons besoin d’un total de 154 caractères pour définir le mot de passe pour 'sa' : 26 pour l’instruction UPDATE, 16 pour la clause WHERE, 4 pour 'sa', et 2 pour les guillemets entourés de QUOTENAME(@loginname) : 200 - 26 - 16 - 4 - 2 = 154. Toutefois, étant donné que @new est déclaré comme sysname, cette variable ne peut contenir que 128 caractères. Nous pouvons surmonter cela en passant quelques guillemets simples dans @new.

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variable.
DECLARE @command VARCHAR(200)

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password=' + QUOTENAME(@new, '''')
    + ' WHERE username=' + QUOTENAME(@loginname, '''') 
    + ' AND password=' + QUOTENAME(@old, '''')

-- Execute the command.
EXEC (@command);
GO

En passant 154 caractères dans une mémoire tampon de 128 caractères, un attaquant peut définir un nouveau mot de passe pour sa sans connaître l’ancien.

EXEC sp_MySetPassword 'sa',
    'dummy',
    '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012'''''''''''''''''''''''''''''''''''''''''''''''''''

Pour cette raison, vous devez utiliser une mémoire tampon de grande taille pour une variable de commande ou exécuter directement Transact-SQL dynamique à l’intérieur de l’instruction EXECUTE.

Troncature lors de l'utilisation de QUOTENAME(@variable, '''') et REPLACE().

Les chaînes renvoyées par QUOTENAME() et REPLACE() seront tronquées en mode silencieux si elles dépassent l'espace attribué. La procédure stockée créée dans l'exemple suivant illustre les conséquences possibles.

Dans cet exemple, les données stockées dans des variables temporaires sont tronquées, car la taille de la mémoire tampon de @login, @oldpassword et @newpassword est de 128 caractères, mais QUOTENAME() peut retourner jusqu’à 258 caractères. Si @new elle contient 128 caractères, @newpassword peut être 123... n, où n est le 127e caractère. Étant donné que la chaîne retournée par QUOTENAME() est tronquée, elle peut ressembler à l’instruction suivante :

UPDATE Users SET password ='1234...[127] WHERE username=' -- other stuff here

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);

SET @login = QUOTENAME(@loginname, '''');
SET @oldpassword = QUOTENAME(@old, '''');
SET @newpassword = QUOTENAME(@new, '''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users set password = ' + @newpassword
    + ' WHERE username = ' + @login
    + ' AND password = ' + @oldpassword;

-- Execute the command.
EXEC (@command);
GO

Par conséquent, l’instruction suivante définit les mots de passe de tous les utilisateurs à la valeur qui a été transmise dans le code précédent.

EXEC sp_MyProc '--', 'dummy', '12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678'

Vous pouvez forcer la troncature de chaîne en dépassant l'espace de la mémoire tampon attribué lors de l'utilisation de REPLACE(). La procédure stockée créée dans l'exemple suivant illustre les conséquences possibles.

Dans cet exemple, les données sont tronquées, car les mémoires tampon attribuées pour @login, @oldpassword et @newpassword ne peuvent contenir que 128 caractères, mais QUOTENAME() peut retourner jusqu’à 258 caractères. Si @new contient 128 caractères, @newpassword peut être '123...n', où n est le 127e caractère. Étant donné que la chaîne retournée par QUOTENAME() est tronquée, elle peut ressembler à l’instruction suivante :

UPDATE Users SET password='1234...[127] WHERE username=' -- other stuff here

CREATE PROCEDURE sp_MySetPassword
    @loginname SYSNAME,
    @old SYSNAME,
    @new SYSNAME
AS
-- Declare variables.
DECLARE @login SYSNAME;
DECLARE @newpassword SYSNAME;
DECLARE @oldpassword SYSNAME;
DECLARE @command VARCHAR(2000);

SET @login = REPLACE(@loginname, '''', '''''');
SET @oldpassword = REPLACE(@old, '''', '''''');
SET @newpassword = REPLACE(@new, '''', '''''');

-- Construct the dynamic Transact-SQL.
SET @command = 'UPDATE Users SET password = '''
    + @newpassword + ''' WHERE username = '''
    + @login + ''' AND password = ''' + @oldpassword + '''';

-- Execute the command.
EXEC (@command);
GO

Comme dans le cas de QUOTENAME(), la troncature de chaîne par REPLACE() peut être évitée en déclarant des variables temporaires de taille suffisante pour tous les cas. Si possible, vous devez appeler QUOTENAME() ou REPLACE() directement à l’intérieur de Transact-SQL dynamique. Sinon, vous pouvez calculer la taille requise de la mémoire tampon de la manière suivante. Pour @outbuffer = QUOTENAME(@input), la taille de @outbuffer doit être 2 * (len(@input) + 1). Lorsque vous utilisez REPLACE() et des apostrophes doubles, comme dans l’exemple précédent, une mémoire tampon de 2 * len(@input) est suffisante.

Le calcul suivant couvre tous les cas :

WHILE LEN(@find_string) > 0, required buffer size =
    ROUND(LEN(@input) / LEN(@find_string), 0)
        * LEN(@new_string) + (LEN(@input) % LEN(@find_string))

Troncature lors de l'utilisation de QUOTENAME(@variable, ']')

La troncature peut se produire quand le nom d'un moteur de base de données sécurisable est transmis à des instructions utilisant le formulaire QUOTENAME(@variable, ']'). L’exemple suivant illustre ce cas de figure.

Dans cet exemple, @objectname doit autoriser 2 * 258 + 1 caractères.

CREATE PROCEDURE sp_MyProc
    @schemaname SYSNAME,
    @tablename SYSNAME
AS
-- Declare a variable as sysname. The variable will be 128 characters.
DECLARE @objectname SYSNAME;

SET @objectname = QUOTENAME(@schemaname) + '.' + QUOTENAME(@tablename);
    -- Do some operations.
GO

Lorsque vous concaténez des valeurs de type sysname, vous devez utiliser des variables temporaires de taille suffisante pour contenir le maximum de 128 caractères par valeur. Dans la mesure du possible, appelez QUOTENAME() directement à l’intérieur de Transact-SQL dynamique. Sinon, vous pouvez calculer la taille requise de la mémoire tampon comme expliqué dans la section précédente.