Corriger les injections SQL : un ORM ne suffit pas
8 juin 2016
0 minutes de lectureL’un des types de vulnérabilités les plus dangereux et les plus répandus est l’injection SQL, qui permet aux attaquants d’accéder à votre base de données backend. L’utilisation de requêtes préparées et du mapping objet-relationnel (ORM) est un bon moyen de se protéger contre les injections SQL, mais cela ne suffit pas. Comme le montre cet article, les packages ORM tels que Sequelize et MySQL peuvent présenter des failles qui vous exposent à des risques. Pour vous protéger efficacement, vous devez faire davantage.
Dans notre rapport 2019 sur l’état de la sécurité open source, nous avons appris que les vulnérabilités d’injection SQL restent une source courante de préoccupations en matière de sécurité, avec un pic de 16 vulnérabilités détectées dans des bibliothèques du dépôt PHP Packagist.
En bref
Une vulnérabilité d’injection SQL, ou SQLi, permet à un attaquant d’ajouter — d’« injecter » — du texte non structuré dans une commande SQL, ce qui peut entraîner des conséquences imprévues. Pour vous protéger contre les injections SQL, vous pouvez utiliser un package ORM, qui traduit vos objets et les actions effectuées sur ceux-ci en SQL. Avec ces packages, vous ne rédigez jamais directement de SQL et ne risquez donc pas d’y introduire accidentellement des injections malveillantes. Hourra !
Mais il est facile d’oublier que même si vous ne rédigez pas de SQL, quelqu’un le fait bel et bien. En effet, les packages ORM de npm doivent quand même convertir ces actions en SQL. Comme tous les packages, ce sont des logiciels, et les logiciels peuvent contenir des bugs. Au cours de l’année écoulée, 4 vulnérabilités d’injection SQL ont été signalées dans les deux principaux packages ORM de npm, sequelize et node-mysql : le risque est donc bien réel, et non théorique. Ces problèmes ont été corrigés dans les dernières versions des packages, mais vous utilisez peut-être d’anciennes versions, et de nouvelles vulnérabilités pourraient être découvertes.
Cet article présente quelques exemples de ces failles et explique quelles autres couches de défense mettre en place, notamment :
Tester vos dépendances pour détecter les vulnérabilités
Valider les entrées, même avec un ORM
Avant d’examiner les scénarios problématiques, rappelons rapidement ce qu’est une injection SQL, les moyens de s’en protéger et le rôle d’un ORM.
Exemple simple d’injection SQL
Imaginez une application de liste de tâches dotée d’une fonction de recherche permettant de trouver les éléments contenant un texte donné. La recherche s’effectue au moyen d’une requête à la base de données, implémentée avec le package sequelize. En supposant que la connexion à la base de données est déjà configurée, voici la fonction de recherche backend :
Dans un cas d’utilisation normal, l’utilisateur saisit une valeur simple (par exemple Buy), qui génère la requête attendue ci-dessous et renvoie toutes les tâches d’achat correspondantes de notre liste :
Cependant, un attaquant peut volontairement saisir une apostrophe (') pour sortir du contexte de chaîne prévu et intervenir directement dans la requête. Il peut, par exemple, saisir la valeur ') UNION SELECT username||'_'||password FROM Users --, ce qui génère la requête suivante :
Si nous avons une table Users contenant les champs password et username, la requête ci-dessus ajoutera à notre liste de tâches TODO tous les noms d’utilisateur et mots de passe. Le reste du texte est facilement mis en commentaire à l’aide de la commande --, afin de préserver l’intégrité de la requête SQL.
Cette implémentation vulnérable peut sembler particulièrement naïve, mais ses variantes sont assez fréquentes. Dans tous les cas, une forme d’entrée utilisateur non vérifiée est ajoutée à une requête SQL brute, ce qui permet de sortir du contexte initial (par exemple, une chaîne) et de déclencher des actions imprévues. L’attaque présentée est assez simple et directe, mais les véritables attaquants essaient progressivement de nombreuses variantes et n’ont besoin que d’une seule réussite.
Impact et correction
L’injection SQL est une vulnérabilité extrêmement grave. Dans la plupart des cas, une seule injection SQL, quel que soit l’endroit de votre site Web, peut finir par permettre l’exécution de n’importe quelle requête sur la base de données, ainsi que l’extraction et la modification de ses données. Comme les bases de données contiennent souvent les informations les plus sensibles du système, accorder un tel accès aux attaquants est dévastateur.
Il existe deux principales méthodes, qui ne s’excluent pas mutuellement, pour prévenir les injections SQL : la validation des entrées et les requêtes préparées.
Validation des entrées
Comme les autres attaques par injection, l’injection SQL commence par une entrée malveillante de l’utilisateur. Pour l’empêcher, vous pouvez vérifier que les données fournies par l’utilisateur sont valides. Dans le cas contraire, vous pouvez annuler complètement l’opération ou supprimer les caractères potentiellement dangereux.
La validation des entrées peut s’appuyer sur un modèle de sécurité négatif ou positif.
Un modèle de sécurité négatif consiste à interdire certains caractères ou motifs dangereux. Dans l’exemple ci-dessus, nous pourrions par exemple interdire l’apostrophe qui permettait de sortir du contexte de chaîne. Malheureusement, SQL est complexe et il est difficile de recenser tous les caractères potentiellement dangereux. Dans cet exemple, nous devrions également bloquer les caractères tels que le retour arrière (b), la barre oblique inverse (\), le caractère nul (x00) et probablement plusieurs autres. La liste s’allonge encore si nous prenons en charge plusieurs types de bases de données. Cela dit, le blocage des caractères dangereux reste une mesure d’atténuation assez simple et efficace.
Un modèle de sécurité positif consiste à n’autoriser que certains caractères. Dans l’exemple ci-dessus, limiter l’entrée aux lettres, aux chiffres et aux espaces (/a-zA-Z0-9 /) aurait efficacement éliminé le risque. Cette approche, également appelée liste d’autorisation, est généralement préférable du point de vue de la sécurité, car elle évite les surprises liées à des valeurs auxquelles vous n’aviez pas pensé. Elle risque toutefois davantage de bloquer des caractères légitimes, en particulier si vous ajoutez la prise en charge des jeux de caractères Unicode.
Requêtes préparées et ORM
Si les attaques par injection SQL commencent avec l’entrée, elles aboutissent à la requête SQL, ce qui nous donne une deuxième occasion de les prévenir. Le problème vient essentiellement de la concaténation de chaînes utilisée pour créer la requête SQL. Si nous utilisions plutôt un modèle de requête, nous pourrions indiquer à la base de données (ou à la bibliothèque de connexion) qu’il s’agit d’une valeur de chaîne et la laisser encoder cette chaîne comme il convient. C’est la solution évoquée au début de cet article.
Ces modèles sont surtout connus sous le nom de « requêtes préparées », ou parfois de « requêtes paramétrées ». Le package sequelize utilisé ci-dessus les prend également en charge. Nous pouvons donc corriger la vulnérabilité en modifiant notre fonction comme suit :
Sequelize comprendra que ? représente une valeur et non une commande SQL, et l’encodera correctement pour empêcher toute sortie du contexte de valeur.
Les requêtes préparées ne sont pas seulement un bon moyen de sécuriser votre code : elles le rendent aussi plus lisible et plus facile à maintenir. Autrement dit, chaque fois que vous êtes tenté de concaténer des valeurs dans une instruction SQL, résistez à cette envie et utilisez plutôt une requête préparée.
Pour aller plus loin, une approche encore plus programmatique de SQL consiste à utiliser le mapping objet-relationnel (ORM). L’ORM associe les tables de votre base de données à vos objets, ce qui vous permet de lire, d’écrire et d’interroger des objets entiers. Comme l’ORM réduit davantage le recours au SQL explicite, c’est aussi un bon moyen d’éviter les injections SQL.
Quand les ORM sont vulnérables : Sequelize
Les requêtes préparées et les ORM sont deux bons moyens de confier la responsabilité de l’encodage à des « experts » : les packages spécialisés dans cette tâche. Mais être expert ne signifie pas être à l’abri des bugs… Comme indiqué au début, cela s’est confirmé l’année dernière avec 4 vulnérabilités d’injection SQL dans deux des principaux packages ORM de npm, sequelize et node-mysql.
Ces vulnérabilités étaient systématiquement dues à des paramètres non validés dans différents appels ORM et à des requêtes préparées. Au bout du compte, ces appels de fonctions ORM et ces requêtes préparées doivent tout de même convertir les paramètres en instruction SQL, et peuvent omettre de les échapper ou de les valider au passage.
Examinons quelques vulnérabilités de sequelize pour mieux comprendre ce qui s’est passé. Il convient de noter que ces vulnérabilités sont désormais corrigées et que les développeurs de Sequelize ont réagi rapidement après la découverte et le signalement des problèmes. Vous trouverez la liste complète des vulnérabilités de Sequelize dans la base de données des vulnérabilités de Snyk, accompagnée de conseils de correction.
La première, divulguée en janvier 2016 et corrigée dans la version 3.17.0, concernait la fonction findAll, souvent utilisée pour interroger des objets de la base de données avec un ORM. Lors de la conversion de ses paramètres en SQL, la fonction ne restreignait pas les valeurs du paramètre LIMIT, ouvrant ainsi la voie à une vulnérabilité d’injection SQL.
Voici un exemple de la façon dont cette vulnérabilité pouvait être déclenchée sur une liste de tâches TODO.
Si Items contient les champs Username et Desc, la requête obtenue ressemblerait à peu près à ceci :
Une autre faille similaire a été divulguée fin mars 2016. Cette fois, le problème concernait la création de requêtes préparées et la concaténation de valeurs pour l’instruction IN. Voici un exemple d’attaque :
Une fois découvertes, ces vulnérabilités ne sont pas très complexes, mais elles montrent que même les packages populaires ne sont pas infaillibles. SQL est complexe et il est facile de passer à côté de cas limites.
Solution : la défense en profondeur
Si ce n’était pas déjà clair, je vais le dire sans détour : vous devez absolument utiliser un ORM et des requêtes préparées. Ils éliminent la majeure partie du risque d’injection SQL et constituent généralement de bonnes pratiques de développement logiciel. Toutefois, ne croyez pas que ces packages vous rendent totalement invulnérable.
Vous devez également valider les entrées. Vous empêcherez ainsi les données malveillantes d’entrer dans votre système, ce qui est un excellent moyen de réduire les risques. La mise en place de plusieurs défenses de ce type est souvent appelée « défense en profondeur » et c’est une excellente pratique. Si une attaque franchit une couche de défense (la validation des entrées), la deuxième couche devrait la bloquer. Si chaque couche laissait passer 1 % des attaques, deux couches en bloqueraient toutes sauf 0,01 % : les chiffres parlent d’eux-mêmes.
Par ailleurs, vous devez suivre les vulnérabilités connues des packages que vous utilisez et les corriger dès que vous les découvrez. Vous pouvez utiliser Snyk (gratuitement !) pour tester vos applications et détecter les vulnérabilités mentionnées ci-dessus, puis intégrer des tests de vulnérabilité à votre processus de développement pour contribuer à maintenir vos applications exemptes de vulnérabilités.
Apprécié par les développeurs. Les équipes de sécurité lui font confiance.
Les outils Snyk, conçus pour les développeurs, offrent une sécurité intégrée et automatisée qui répond à vos exigences de gouvernance et de conformité.
