Conseils de prévention des injections SQL pour les programmeurs Web
Il est rare de voir un site Web ou une application véritablement statique de nos jours. Au lieu des données codées en dur et des mises en page de tableaux qui étaient si courantes, le paysage du développement Web moderne est dominé par The Feed : un défilement apparemment sans fin de contenu dynamique.
Des flux comme ceux-ci sont créés en extrayant des données d’une base de données. Cela vous permet de filtrer des éléments tels que les tweets ou les mises à jour de statut sans avoir à changer de page, et signifie qu'une bien plus grande partie de votre service peut être automatisée qu'au cours des décennies précédentes. Les types de bases de données les plus courants sont basés sur le très populaire langage de requête structuré (SQL), qui est simplement le langage de programmation que la base de données utilise pour stocker et récupérer des données. Chaque tableau de la base de données peut être considéré comme similaire à une feuille de calcul dont chaque cellule contient les points de données.
L'injection SQL est un type d'attaque spécial qui cible ces types d'applications en ligne. Même si la base de données elle-même peut être protégée contre le piratage, le point faible de ces attaques réside dans l'application et son niveau d'accès à la base de données. L'attaque incite l'application à transmettre des commandes SQL supplémentaires à la base de données, généralement dans le but soit d'obtenir des droits d'accès élevés, soit de faire en sorte que la base de données crache plus d'informations qu'elle n'est censée le faire.
Nous aborderons les trois principales façons dont un programmeur peut renforcer son code contre ce type d’attaque. Nous fournirons également des exemples de code Java, PHP, Python et Perl pour vous aider à comprendre comment ces solutions sont déployées. Il existe également des moyens permettant à un administrateur de base de données ou de serveur de renforcer son serveur en collaboration avec le programmeur. Ceux-ci sont:
- Utilisation d'espaces réservés dans les requêtes préparées
- Désinfection des entrées de l'utilisateur
- L'utilisation de procédures stockées
Et pour les administrateurs du serveur/base de données :
- Droits d'accès de moindre privilège
Espaces réservés
Nous aborderons d’abord l’utilisation d’espaces réservés dans les commandes SQL préparées et intégrées dans le code source du programme. L'espace réservé est simplement « ? » et il est placé dans la requête SQL à la place d'une valeur qui n'a pas encore été fournie. Le programme devrait alors remplacer ce « ? » par une valeur fournie soit par l'utilisateur, soit par une autre section du programme.
Pour illustrer le concept, prenons l'exemple de requête SQL suivant :
|_+_|Le résultat de cette requête sera chaque colonne de données de la table du client relative au client avec l'ID client de 1078. Supposons maintenant que le programme demande à l'utilisateur de fournir son propre ID client. La version non sécurisée de cette même requête pourrait ressembler à ceci :
|_+_|Dans cet exemple, l'utilisateur qui accède au programme doit simplement saisir son identifiant dans le champ correct pour obtenir les détails de son compte. Cependant, si l'utilisateur brise la syntaxe de l'instruction (par exemple en entrant « 0 OR 1=1 » au lieu d'un ID client valide), alors la requête de base de données sera désormais envoyée comme :
|_+_|Puisque 1 sera toujours égal à 1, la requête ci-dessus crachera le contenu de chaque ligne de la table du client. Cela inclut chaque ID client, ainsi que toutes les informations stockées dans ce tableau concernant la base de clients d'une entreprise ; pas quelque chose auquel vous voulez que n’importe qui ait accès. La première façon d'éviter ce type de fuite consiste à utiliser des instructions préparées avec des espaces réservés pour les valeurs générées dynamiquement.
Déclarations préparées
SQL inclut déjà une option permettant d'utiliser des espaces réservés dans une instruction préparée SQL générée dynamiquement. L'espace réservé peut ensuite être remplacé par la valeur appropriée juste avant d'exécuter la requête. Une instruction préparée est simplement une requête SQL configurée dès le début dans le but d'être utilisée plusieurs fois sans avoir à soumettre à nouveau l'intégralité de la requête à la base de données. Au lieu de cela, l'instruction préparée est envoyée une fois, puis les différentes valeurs sont envoyées séquentiellement.
Dans votre programme, vous préparez votre requête ou instruction SQL avec un espace réservé à la place du champ de données que vous souhaitez que l'utilisateur fournisse. Par exemple, avec notre exemple de requête ci-dessus, le programme demande à l'utilisateur son identifiant client afin de lui permettre de consulter les détails de son compte. Au lieu de placer une variable directement dans la requête, vous souhaitez y placer un « ? » Cela ressemblera à ceci :
|_+_|Le programme est ensuite chargé de fournir une valeur pour remplacer l'espace réservé. Il peut s'agir d'une variable préalablement définie, d'une entrée utilisateur ou même d'une liste de valeurs provenant entièrement d'un autre programme. Une fois la valeur remplacée, la requête peut être exécutée et les résultats traités.
Les espaces réservés sont brillants par leur simplicité. La base de données elle-même filtrera de nombreuses données inattendues ou potentiellement dangereuses provenant de l'utilisateur en fonction de ce vers quoi pointe l'espace réservé. Par exemple, si le champ de votre nom d'utilisateur n'accepte que les entrées alphanumériques, il supprimera simplement tout élément supplémentaire de la requête sans le traiter du tout.
Bien entendu, chaque fois que la valeur doit être fournie par un utilisateur, le programme doit s'assurer que l'entrée de l'utilisateur ne contient pas d'injection de code SQL supplémentaire. Le programme devra d'abord nettoyer l'entrée avant d'exécuter la requête.
Nettoyage des entrées utilisateur
Nettoyer les entrées de l'utilisateur signifie simplement s'assurer que l'utilisateur ne saisit que ce que vous attendez de lui. Tout ce que l'utilisateur saisit et qui ne correspond pas à ce que le programme attend est automatiquement rejeté et une erreur générée, avant que quoi que ce soit n'arrive dans la base de données. Cette méthode de validation uniquement est préférable au fait de laisser l'utilisateur saisir ce qu'il souhaite, puis d'essayer de créer une expression régulière à rejeter ou à générer une erreur lorsqu'un code SQL supplémentaire est détecté.
Dans notre exemple ci-dessus, supposons pour le moment que le clientID soit une chaîne de caractères. Le programme peut limiter la saisie de l'utilisateur à une liste blanche de caractères viables, avec une longueur maximale ne dépassant pas la longueur de l'ID client le plus long et n'autorisant aucun espace. Si l'ID client était un nombre, limitez la saisie de l'utilisateur à des entiers d'une certaine longueur : ni plus, ni moins.
Pour les champs qui nécessitent un mélange de différents types de caractères, comme une adresse e-mail, le programme peut toujours avoir besoin d'une expression régulière pour nettoyer la saisie de l'utilisateur. Premièrement, pour s'assurer que le programme reçoit une adresse e-mail réelle, et deuxièmement, pour s'assurer qu'une adresse e-mail est tout ce qui est fourni.
Une expression régulière est un modèle de recherche défini basé sur une chaîne de caractères définie. Un exemple d'expression régulière spécifiquement destinée à la validation des adresses e-mail ressemble à ceci :
|_+_|Il est de loin préférable de mettre sur liste blanche les entrées des utilisateurs en acceptant uniquement les informations attendues et en rejetant tout le reste plutôt que de mettre sur liste noire les entrées des utilisateurs en recherchant dans ce qui a été fourni tout ce qui semble suspect. Avec une liste noire, il est toujours possible que l'expression régulière manque quelque chose de vraiment inattendu.
À titre d'exemple, il y a plusieurs années, un pirate informatique a découvert qu'il pouvait entrez une valeur négative lors d’un transfert de fonds de son compte bancaire vers le compte bancaire d’un ami, via l’application de banque en ligne de sa banque. Le résultat a été qu’il a effectivement volé de l’argent sur le compte de son ami. Étant une personne éthique, il l'a non seulement montré à son ami pour rire, mais il l'a également fait savoir à la banque.
Les programmeurs de l’application en ligne de cette banque n’avaient pas prédit qu’un utilisateur injecterait une valeur négative dans le champ de saisie pour un transfert de fonds d’un compte à un autre, ce n’était donc pas dans leur liste noire. Il s’agit de la forme la plus simple d’attaque par injection SQL que j’ai pu trouver sur Internet et qui ne nécessite aucune connaissance en SQL ou en programmation. Il fallait juste un peu de curiosité et un compte utilisateur valide avec des droits suffisants pour effectuer une transaction.
Procédures stockées
Les procédures stockées peuvent être très sécurisées lorsque les transactions SQL sous-jacentes sont statiques. Par exemple, lorsque les données accessibles ne dépendent pas de l'entrée de l'utilisateur, d'un autre programme ou d'une variable définie précédemment dans le programme et extraite d'une source autre que le serveur local. Si les données présentées sont basées sur une variable d'environnement, comme la date actuelle, l'emplacement géographique de l'utilisateur ou un nom d'utilisateur, alors les données sont considérées comme statiques.
Toutefois, si la transaction SQL est basée sur des données générées dynamiquement, elle doit être traitée comme suspecte jusqu'à ce qu'elle puisse être validée. Tout ce qui est fourni par l'utilisateur pendant l'exécution du programme est automatiquement considéré comme dangereux. De même, toute information générée par un autre programme doit être considérée comme suspecte jusqu'à ce qu'elle soit nettoyée ou éliminée.
Lors de la gestion de contenu généré dynamiquement, les procédures stockées sont tout aussi sensibles aux attaques par injection que toute autre interaction SQL. Ils peuvent également bénéficier des mêmes tactiques que celles utilisées par les instructions préparées, notamment en utilisant des espaces réservés et en nettoyant les entrées avant qu'elles ne soient transmises à la base de données.
Comme son nom l'indique, la procédure stockée est en fait créée dans la base de données elle-même, alors qu'une instruction préparée est configurée dans le programme juste avant d'interroger la base de données. Lorsque vous avez déjà une procédure stockée dans la base de données, le programme doit simplement appeler cette procédure en fournissant la ou les valeurs qu'il s'attend à recevoir.
Pour utiliser l'exemple de requête SQL précédent, vous pouvez créer une procédure stockée dans la base de données elle-même à l'aide de la commande CREATE PROCEDURE comme ceci :
|_+_|Votre base de données dispose désormais d'une procédure stockée appelée sp_getClientData. Pour l'utiliser, le programme n'a qu'à l'appeler et à fournir une valeur pour remplacer l'espace réservé. Jetez un œil aux exemples de code à la fin de l’article pour appeler une procédure stockée dans chacun des langages de programmation abordés.
Astuce pour renforcer le serveur : droits d'accès au moindre privilège
Le principal conseil pour rendre le serveur de base de données lui-même un peu plus sécurisé lorsqu'il s'agit de tout type de programme est de minimiser les droits d'accès de tous les différents comptes « utilisateurs » dans la base de données. Il ne doit y avoir qu'un seul compte DBA ou administrateur, et ces informations d'identification ne doivent jamais être utilisées dans aucun programme ou application. En fait, tout programme ayant besoin d'accéder à une base de données doit avoir des comptes différents en fonction des privilèges dont il a besoin lors de sa connexion.
Lors de la création de ces comptes, le vieil adage « moins c'est plus » entre en jeu. Commencez sans droits, puis ajoutez uniquement les droits nécessaires pour exécuter la fonction appelée. Dans le même temps, toutes les vues ou procédures stockées doivent être créées en même temps. Encore une fois, moins c'est mieux lorsqu'il s'agit de sécurité des données.
Si le programme a uniquement besoin de rechercher des informations relatives à un client, il aura besoin d'un compte utilisateur avec un accès en lecture seule aux tableaux spécifiques relatifs aux informations client. En revanche, s’il est censé pouvoir modifier n’importe quelle information relative aux employés de l’entreprise, le compte utilisateur lors du segment de connexion à la base de données de ce programme doit avoir un accès en lecture et en écriture aux tables relatives aux employés de l’entreprise.
Exemples de codes
Mettons donc toutes ces informations dans quelques exemples de code réels. Pour commencer, nous travaillerons avec une base de données MySQL sur l'hôte local appelée « demo ». Par souci de simplicité, cette base de données ne comporte que deux tables, clients et profils. Notez également que le système d'exploitation de ces exemples est Debian Linux, mais que les exemples peuvent également être utilisés sur des serveurs exécutant d'autres systèmes d'exploitation.
Les exemples de code effectuent tous les mêmes tâches, mais dans des langages de programmation différents. Les langages sont, sans ordre particulier, Java, PHP, Python et Perl.
Une dernière remarque. Dans les exemples suivants, le processus de nos programmes se déroule comme suit :
- Connectez-vous à la base de données
- Préparez une instruction avec un ou plusieurs espaces réservés ou appelez une procédure stockée
- Obtenez l’entrée requise de l’utilisateur
- Désinfectez les entrées de l’utilisateur
- Injecter l'entrée de l'utilisateur dans la transaction SQL en remplaçant le(s) espace(s) réservé(s)
- Exécuter la requête SQL
- Afficher les résultats de la requête SQL
- Déconnectez-vous de la base de données lorsque vous avez terminé
Java
Obtenir les commentaires d'un utilisateur est assez simple en Java :
|_+_|Pour se connecter à une base de données, le programme doit connaître l'adresse du serveur, le nom de la base de données sur ce serveur et les informations de connexion d'un compte disposant des droits d'accès nécessaires à la ou aux commandes SQL à exécuter. lors de sa session de connexion à la base de données :
|_+_|Pour préparer une instruction paramétrée, vous avez besoin de l'espace réservé dans l'instruction SQL, de l'instruction transmise à la base de données et de la valeur fournie par l'utilisateur fournie à la place de l'espace réservé :
Requête de chaîne = 'SELECT * FROM clients WHERE clientID = ?