Skip to main content

Injection SQL : 8 bonnes pratiques pour prévenir les attaques par injection SQL

Écrit par

26 mars 2021

0 minutes de lecture

L’injection SQL est l’une des vulnérabilités les plus dangereuses pour les applications en ligne. Elle se produit lorsqu’un utilisateur ajoute des données non fiables à une requête de base de données, par exemple en remplissant un formulaire Web. Si l’injection SQL est possible, des attaquants avisés peuvent créer des entrées utilisateur pour dérober des données précieuses, contourner l’authentification ou corrompre les enregistrements de votre base de données.

Il existe différents types d’attaques par injection SQL, mais elles ont toutes, en général, une cause similaire. Les données non fiables saisies par l’utilisateur sont concaténées à la chaîne de requête. L’entrée de l’utilisateur peut donc modifier l’intention initiale de la requête.

Voici quelques exemples d’injection SQL :

  • Ajouter une condition booléenne toujours vraie à une clause WHERE, comme ' OR 1=1

  • Échapper une partie de la requête en saisissant un commentaire de fin de ligne --

  • Terminer la requête initiale et en démarrer une nouvelle '; DROP TABLE USERS;

  • Relier des données issues de plusieurs tables à l’aide de UNION

Dans cette fiche pratique, je présente huit bonnes pratiques que tout développeur d’applications peut adopter pour prévenir les attaques par injection SQL. C’est parti pour sécuriser votre application contre les injections SQL.

Fiche pratique Snyk présentant huit bonnes pratiques pour prévenir les attaques par injection SQL, notamment la validation, les requêtes préparées, la sécurité des ORM et le contrôle des entrées.

Télécharger la fiche pratique

  1. Ne vous fiez pas à la validation des entrées côté client

  2. Utilisez un utilisateur de base de données aux privilèges restreints

  3. Utilisez des instructions préparées et paramétrez les requêtes

  4. Analysez votre code pour détecter les vulnérabilités d’injection SQL

  5. Utilisez une couche ORM

  6. Ne vous fiez pas aux listes de blocage

  7. Validez les entrées

  8. Soyez prudent avec les procédures stockées

1. Ne vous fiez pas à la validation des entrées côté client.

La validation des entrées côté client est très utile. Elle permet d’empêcher les données non valides d’atteindre la logique de votre système. Malheureusement, cela ne fonctionne que pour les utilisateurs qui n’ont pas de mauvaises intentions et souhaitent utiliser le système comme prévu. Fournir directement à l’utilisateur un retour indiquant qu’une valeur n’est pas valide est très utile et améliore l’expérience. Vous devriez donc utiliser la validation côté client pour améliorer l’expérience utilisateur.

En matière d’injection SQL, ce n’est toutefois pas une méthode sur laquelle vous devriez vous appuyer. Il est possible de supprimer la validation côté client en modifiant le code JavaScript chargé dans le navigateur. De plus, dans une architecture client-serveur, il est assez facile d’effectuer un appel HTTP simple vers le backend avec un paramètre qui provoque une injection SQL, à l’aide d’outils comme Postman ou de commandes curl classiques.

Vous devriez valider les données côté serveur, idéalement aussi près de leur source que possible. Dans ce cas, au moment de créer la requête SQL. Tout ce qu’un client vous envoie doit être considéré comme potentiellement dangereux. Par conséquent, se fier à la validation côté client pour se prémunir contre l’injection SQL est une très mauvaise idée.

2. Utilisez un utilisateur de base de données aux privilèges restreints

Comme indiqué précédemment, il existe différents types d’attaques par injection SQL. Certaines sont plus dangereuses que d’autres. Prenons l’exemple d’une requête SQL comme "SELECT * FROM USER WHERE USERID = '" + userid +"'". L’injection " foo' OR '1'='1 " renverra tous les utilisateurs, ce qui est déjà grave. Cependant, " '; UPDATE message SET password = 'EVIL" causera encore plus de problèmes, car l’intrus aura alors modifié toutes les entrées.

Lorsque vous créez un utilisateur de base de données pour votre application, vous devez réfléchir aux privilèges à lui accorder. L’application doit-elle pouvoir lire, écrire et modifier toutes les bases de données ? Et tronquer ou supprimer des tables ? En limitant les privilèges de votre application sur la base de données, vous pouvez réduire les conséquences d’une injection SQL. Il est probablement préférable de ne pas utiliser un seul utilisateur de base de données pour votre application, mais d’en créer plusieurs et de les associer à des rôles spécifiques. Les problèmes de sécurité résultent souvent d’un effet domino : vous devez donc surveiller chaque maillon de la chaîne pour éviter de graves dommages.

3. Utilisez des instructions préparées et paramétrez les requêtes

De nombreux langages proposent des fonctionnalités intégrées qui aident à prévenir l’injection SQL. Lorsque vous écrivez des requêtes SQL, vous pouvez utiliser une instruction préparée pour compiler la requête. Elle permet de paramétrer les requêtes, une technique qui consiste à créer des instructions SQL de manière dynamique. Vous créez une requête de base avec des espaces réservés, puis vous y associez en toute sécurité les paramètres fournis par l’utilisateur.

Avec de véritables instructions préparées et des requêtes paramétrées, c’est la base de données elle-même qui se charge de l’échappement. Dans un premier temps, elle établit le plan d’exécution de la requête à partir de la chaîne contenant les espaces réservés. Dans un second temps, les paramètres (non fiables) sont envoyés à la base de données. Le plan de requête étant déjà créé, les paramètres ne peuvent plus l’influencer. Cela empêche toute injection.

Exemple en Java :

String query = "SELECT * FROM USERS WHERE username LIKE ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, parameter);
ResultSet result = statement.executeQuery();

Exemple en Python avec le connecteur MySQL :

cursor = conn.cursor(prepared=True)
params = ("foo",)
cursor.execute("SELECT * FROM USERS WHERE username = %s", params)

Exemple en JavaScript avec mysql2 :

connection.query("SELECT * FROM USERS WHERE username = ?",[
     req.body.username
    ],function(error, results){}); 
//emulates a prepared statement

//OR

connection.execute("SELECT * FROM USERS WHERE username = ?",[
     req.body.username
    ],function(error, results){});
//prepared statement

Il existe plusieurs façons de procéder en JavaScript, par exemple avec une base de données MySQL. Attention : avec .query(), vous n’utilisez pas une véritable instruction préparée. Dans ce cas, la substitution des paramètres est effectuée côté client : vous simulez donc une instruction préparée. Pour créer une véritable instruction préparée dans la base de données, utilisez la fonction .execute().

4. Analysez votre code pour détecter les vulnérabilités d’injection SQL

Écrire du code personnalisé est sans doute facile, mais les erreurs sont vite commises. Pour vérifier votre code, vous avez peut-être mis en place des processus comme la revue de code ou la programmation en binôme. Mais la personne qui révise votre code ou qui travaille en binôme avec vous connaît-elle bien la sécurité ? Peut-elle repérer une faille d’injection SQL dans votre code ? Dans tous les cas, il serait utile d’examiner automatiquement votre code personnalisé pour détecter d’éventuelles vulnérabilités, comme l’injection SQL. Avec un outil SAST (test de sécurité statique des applications) comme Snyk Code, vous pouvez analyser automatiquement votre code à la recherche de vulnérabilités de sécurité, comme l’injection SQL. Vous pouvez facilement automatiser cette analyse dans votre SDLC, par exemple en connectant votre dépôt Git à Snyk.

Écran Snyk Code montrant une entrée HTTP non assainie transmise à executeQuery dans MessageRepo.java

5. Utilisez une couche ORM

Vous pouvez également envisager d’utiliser une couche de mapping objet-relationnel (ORM). Une couche ORM transforme les données de la base de données en objets, et inversement. Une bibliothèque ORM réduit le nombre de requêtes SQL explicites et, par conséquent, le risque d’injection SQL.

Hibernate pour Java et Entity Framework pour C# sont deux exemples de bibliothèques ORM populaires. Grâce au typage fort de ces langages, il est généralement possible de générer le mapping entre les objets et les tables de la base de données. Vous n’avez ainsi même pas besoin d’écrire vous-même des requêtes SQL.

Le problème se pose toutefois lorsque vous devez créer des requêtes personnalisées. Hibernate pour Java dispose de son propre langage de requête, Hibernate Query Language (HQL). Lorsque vous compilez des requêtes HQL, gardez à l’esprit le risque d’injection et utilisez la fonction createQuery(), qui fonctionne comme une instruction préparée.

Il existe également des bibliothèques ORM JavaScript bien connues, comme sequelize. Avec sequelize, vous pouvez définir la correspondance entre les valeurs et des types spécifiques dans la base de données. Mais soyons réalistes : au final, une bibliothèque ORM doit convertir la logique en instructions SQL. Nous devons donc faire confiance à ces bibliothèques pour échapper correctement les paramètres.

Pour vous assurer que votre bibliothèque ORM ne présente pas de problèmes d’injection SQL, recherchez-y les vulnérabilités connues. L’utilisation d’une version obsolète ou vulnérable de sequelize ou d’hibernate peut toujours vous exposer à des risques. Vérifiez votre projet avec Snyk Open Source pour repérer les risques cachés d’injection SQL dans vos bibliothèques, ainsi que de nombreux autres problèmes.

6. Ne vous fiez pas aux listes de blocage

Beaucoup d’entre vous connaissent sans doute déjà ce conseil, mais je le répète : n’utilisez pas de listes de blocage pour vos paramètres. Cette approche consiste à définir un ensemble de règles décrivant les entrées vulnérables. Si une entrée correspond à ces règles, la requête est bloquée. Cependant, si les règles sont trop permissives, une entrée malveillante peut tout de même passer. Si elles sont trop strictes, elles bloqueront des entrées valides.

Imaginons, par exemple, que nous bloquions toutes les requêtes contenant le mot OR. La règle peut sembler raisonnable, mais « Or » est en réalité un prénom israélien très courant. Cela signifie qu’une partie de mes collègues ne pourrait pas saisir son nom. Même problème avec une apostrophe ' : d’innombrables noms contiennent ce caractère. Pensez à O’Neill et O'Donnell, mais aussi à des prénoms comme Dont’a, par exemple.

7. Validez les entrées

Oui, vous devez toujours valider les entrées ! Même si les instructions préparées avec des requêtes paramétrées constituent la meilleure défense contre l’injection SQL, prévoyez toujours plusieurs niveaux de protection. À l’instar de la limitation des privilèges d’un utilisateur de base de données, la validation des entrées est une bonne pratique qui réduit globalement les risques pour votre application. Il existe également des situations où les instructions préparées ne sont pas disponibles. Certains langages ne prennent pas en charge ce mécanisme, ou d’anciens systèmes de base de données ne permettent pas de fournir les entrées utilisateur sous forme de paramètres. Dans ces cas, la validation des entrées est une solution de remplacement acceptable.

Veillez à utiliser une liste d’autorisation pour valider les entrées, et non une liste de blocage, comme indiqué précédemment. Définissez une règle décrivant tous les formats autorisés, par exemple avec une expression régulière, ou utilisez une bibliothèque bien maintenue. Associez cette approche à des instructions préparées et à des requêtes paramétrées pour mettre en place une défense solide.

8. Soyez prudent avec les procédures stockées

Beaucoup pensent que les procédures stockées permettent de prévenir l’injection SQL. Ce n’est pas toujours le cas. Tout comme les requêtes SQL créées dans votre application, une procédure stockée peut elle aussi faire l’objet d’une injection malveillante.

Comme pour les requêtes SQL de votre application, paramétrez les requêtes dans vos procédures stockées au lieu de concaténer les paramètres. Il est assez facile de prévenir l’injection SQL dans une procédure stockée.

Alors, ne faites pas ceci dans MySQL :

DELIMITER //
CREATE PROCEDURE `FindUsers`(
    IN Username VARCHAR(50)
)
BEGIN

    SET @Statement = CONCAT('SELECT * FROM User WHERE username = ', Username, ' );

    PREPARE stm FROM @Statement;
    EXECUTE stm;

END //
DELIMITER ;

Utilisez plutôt des requêtes paramétrées dans vos procédures stockées :

DELIMITER //
CREATE PROCEDURE `First`(
    IN Username VARCHAR(50)
)
BEGIN

    PREPARE stm FROM 'SELECT * FROM User WHERE username = ?';
    EXECUTE stm USING Username;

END //
DELIMITER ;

Comme indiqué ci-dessus, les mêmes règles s’appliquent aux procédures stockées et au code de votre application. Leur mise en œuvre varie selon les bases de données. Assurez-vous de savoir comment les implémenter pour votre base de données et restez vigilant face aux injections. Même si je pense qu’il vaut mieux intégrer toute la logique dans votre application, une procédure stockée peut être une solution raisonnable si le langage que vous utilisez ne prend pas en charge les instructions préparées.