D'après moi, les attaques par injection SQL peuvent être évitées en:
Je suppose qu'il y a des avantages et des inconvénients pour chacun, mais pourquoi le n ° 2 a-t-il décollé et est-il devenu plus ou moins le moyen de facto de prévenir les attaques par injection? Est-ce simplement plus sûr et moins sujet aux erreurs ou y avait-il d'autres facteurs?
Si je comprends bien, si le n ° 1 est utilisé correctement et que toutes les mises en garde sont prises en compte, il peut être tout aussi efficace que le n ° 2.
Désinfection, filtrage et codage
Il y avait une certaine confusion de ma part entre ce que désinfection, filtrage, et encodage signifiait. Je dirai qu'à mes fins, tout ce qui précède peut être pris en compte pour l'option 1. Dans ce cas, je comprends que la désinfection et le filtrage ont le potentiel de modifier ou supprimer données d'entrée, lors de l'encodage conserve les données telles quelles, mais les code correctement pour éviter les attaques par injection. Je crois que l'échappement de données peut être considéré comme un moyen de l'encoder.
Requêtes paramétrées vs bibliothèque d'encodage
Il y a des réponses où les concepts de parameterized queries
et encoding libraries
qui sont traités de manière interchangeable. Corrigez-moi si je me trompe, mais j'ai l'impression qu'ils sont différents.
Ma compréhension est que encoding libraries
, quelle que soit leur qualité, ils ont toujours le potentiel pour modifier le "programme" SQL, car ils apportent des modifications au SQL lui-même, avant qu'il ne soit envoyé au SGBDR.
Parameterized queries
d'autre part, envoyez le programme SQL au SGBDR, qui optimise ensuite la requête, définit le plan d'exécution de la requête, sélectionne les index à utiliser, etc., puis branchez les données, comme dernière étape à l'intérieur le SGBDR lui-même.
Bibliothèque d'encodage
data -> (encoding library)
|
v
SQL -> (SQL + encoded data) -> RDBMS (execution plan defined) -> execute statement
Requête paramétrée
data
|
v
SQL -> RDBMS (query execution plan defined) -> data -> execute statement
Importance historique
Certaines réponses mentionnent que, historiquement, les requêtes paramétrées (PQ) ont été créées pour des raisons de performances et avant les attaques par injection qui ciblaient les problèmes d'encodage sont devenues populaires. À un moment donné, il est devenu évident que le PQ était également assez efficace contre les attaques par injection. Pour rester dans l'esprit de ma question, pourquoi PQ est-il resté la méthode de choix et pourquoi s'est-elle développée au-dessus de la plupart des autres méthodes en matière de prévention des attaques par injection SQL?
Le problème est que # 1 vous oblige à analyser et interpréter efficacement l'intégralité de la variante SQL sur laquelle vous travaillez afin de savoir si elle fait quelque chose qu'elle ne devrait pas faire. Et gardez ce code à jour lorsque vous mettez à jour votre base de données. Partout vous acceptez l'entrée pour vos requêtes. Et pas le visser.
Alors oui, ce genre de chose arrêterait les attaques par injection SQL, mais il est absurdement plus coûteux à mettre en œuvre.
Parce que l'option 1 n'est pas une solution. Le filtrage et le filtrage signifient rejeter ou supprimer une entrée non valide. Mais toute entrée peut être valide. Par exemple, l'apostrophe est un caractère valide dans le nom "O'Malley". Il suffit de l'encoder correctement avant de l'utiliser en SQL, c'est ce que font les instructions préparées.
Après avoir ajouté la note, il semble que vous demandiez essentiellement pourquoi utiliser une fonction de bibliothèque standard plutôt que d'écrire votre propre code fonctionnellement similaire à partir de zéro? Vous devriez toujours préférer les solutions de bibliothèque standard à l'écriture de votre propre code. C'est moins de travail et plus maintenable. C'est le cas pour la fonctionnalité any, mais surtout pour quelque chose qui est sensible à la sécurité, cela n'a absolument aucun sens de réinventer la roue par vous-même.
Si vous essayez de faire un traitement de chaîne, vous ne générez pas vraiment de requête SQL. Vous générez une chaîne qui peut produire une requête SQL. Il existe un niveau d'indirection qui ouvre beaucoup de place pour les erreurs et les bugs. C'est quelque peu surprenant, étant donné que dans la plupart des contextes, nous sommes heureux d'interagir avec quelque chose par programme. Par exemple, si nous avons une structure de liste et que nous voulons ajouter un élément, nous ne faisons généralement pas:
List<Integer> list = /* a list of 1, 2, 3 */
String strList = list.toString(); /* to get "[1, 2, 3]" */
strList = /* manipulate strList to become "[1, 2, 5, 3]" */
list = parseList(strList);
Si quelqu'un suggère de le faire, vous répondriez à juste titre que c'est plutôt ridicule, et que l'on devrait simplement faire:
List<Integer> list = /* ... */;
list.add(5, position=2);
Cela interagit avec la structure des données à son niveau conceptuel. Il n'introduit aucune dépendance sur la façon dont cette structure peut être imprimée ou analysée. Ce sont des décisions complètement orthogonales.
Votre première approche est comme le premier échantillon (seulement un peu pire): vous supposez que vous pouvez construire par programmation la chaîne qui sera correctement analysée comme la requête que vous souhaitez. Cela dépend de l'analyseur, et de tout un tas de logique de traitement de chaîne.
La deuxième approche de l'utilisation des requêtes préparées ressemble beaucoup plus au deuxième échantillon. Lorsque vous utilisez une requête préparée, vous analysez essentiellement une pseudo-requête qui est légale mais contient des espaces réservés, puis utilisez une API pour remplacer correctement certaines valeurs. Vous n'impliquez plus le processus d'analyse et vous n'avez pas à vous soucier du traitement des chaînes.
En général, il est beaucoup plus facile et beaucoup moins sujet aux erreurs d'interagir avec les choses à leur niveau conceptuel. Une requête n'est pas une chaîne, une requête est ce que vous obtenez lorsque vous analysez une chaîne ou en créez une par programme (ou toute autre méthode vous permettant d'en créer une).
Il y a une bonne analogie ici entre les macros de style C qui remplacent simplement le texte et les macros de style LISP qui génèrent du code arbitraire. Avec les macros de style C, vous pouvez remplacer du texte dans le code source, ce qui signifie que vous avez la possibilité d'introduire des erreurs syntaxiques ou des comportements trompeurs. Avec les macros LISP, vous générez du code sous la forme que le compilateur le traite (c'est-à-dire que vous renvoyez les structures de données réelles que le compilateur traite, pas le texte que le lecteur doit traiter avant que le compilateur puisse y accéder) . Avec une macro LISP, vous ne pouvez cependant pas générer quelque chose qui serait une erreur d'analyse. Par exemple, vous ne pouvez pas générer (let ((a b) a.
Même avec les macros LISP, vous pouvez toujours générer du mauvais code, car vous n'avez pas forcément connaissance de la structure qui est censée être là. Par exemple, dans LISP, (let ((ab)) a) signifie "établir une nouvelle liaison lexicale de la variable a à la valeur de la variable b, puis renvoyer la valeur de a", et - (let (ab) a) signifie "établir de nouvelles liaisons lexicales des variables a et b et les initialiser toutes les deux à zéro, puis renvoyer la valeur de a." Ce sont tous deux syntaxiquement corrects, mais ils signifient des choses différentes. Pour éviter ce problème, vous pouvez utiliser des fonctions plus sémantiques et faire quelque chose comme:
Variable a = new Variable("a");
Variable b = new Variable("b");
Let let = new Let();
let.getBindings().add(new LetBinding(a,b));
let.setBody(a);
return let;
Avec quelque chose comme ça, il est impossible de renvoyer quelque chose qui est syntaxiquement invalide, et c'est beaucoup plus difficile pour retourner quelque chose qui n'est pas accidentellement ce que vous vouliez.
Cela aide à ce que l'option # 2 soit généralement considérée comme une meilleure pratique car la base de données peut mettre en cache la version non paramétrée de la requête. Les requêtes paramétrées sont antérieures de plusieurs années au problème de l'injection SQL (je crois), il se trouve que vous pouvez tuer deux oiseaux avec une pierre.
Dit simplement: ils ne l'ont pas fait. Votre déclaration:
Pourquoi le mécanisme de prévention des injections SQL a-t-il évolué dans le sens de l'utilisation des requêtes paramétrées?
est fondamentalement défectueux. Les requêtes paramétrées existent depuis bien plus longtemps que l'injection SQL n'est au moins largement connue. Ils ont généralement été développés comme un moyen d'éviter la concentation de chaînes dans la fonctionnalité habituelle des applications LOB (Line of Business). Beaucoup - BEAUCOUP - des années plus tard, quelqu'un a trouvé un problème de sécurité avec ladite manipulation de chaîne.
Je me souviens d'avoir fait SQL il y a 25 ans (quand Internet n'était PAS largement utilisé - il ne faisait que commencer) et je me souviens d'avoir fait SQL contre IBM DB5 IIRC version 5 - et qui avait déjà paramétré des requêtes.
En plus de toutes les autres bonnes réponses:
La raison pour laquelle le numéro 2 est meilleur est qu'il sépare vos données de votre code. Dans le n ° 1, vos données font partie de votre code et c'est de là que viennent toutes les mauvaises choses. Avec le n ° 1, vous obtenez votre requête et devez effectuer des étapes supplémentaires pour vous assurer que votre requête comprend vos données comme des données tandis que dans le n ° 2, vous obtenez votre code et son code et vos données sont des données.
Les requêtes paramétrées, en plus de fournir une défense par injection SQL, ont souvent l'avantage supplémentaire d'être compilées une seule fois, puis exécutées plusieurs fois avec des paramètres différents.
Du point de vue de la base de données SQL select * from employees where last_name = 'Smith'
et select * from employees where last_name = 'Fisher'
sont distinctement différents et nécessitent donc une analyse, une compilation et une optimisation distinctes. Ils occuperont également des emplacements séparés dans la zone de mémoire dédiée au stockage des instructions compilées. Dans un système fortement chargé avec un grand nombre de requêtes similaires qui ont des paramètres différents, le calcul et la surcharge de mémoire peuvent être substantiels.
Par la suite, l'utilisation de requêtes paramétrées offre souvent des avantages de performances majeurs.
Attendez mais pourquoi?
L'option 1 signifie que vous devez écrire des routines de nettoyage pour chaque type d'entrée, tandis que l'option 2 est moins sujette aux erreurs et moins de code à écrire/tester/maintenir.
Presque certainement "prendre soin de toutes les mises en garde" peut être plus complexe que vous ne le pensez, et votre langage (par exemple Java PreparedStatement) en a plus sous le capot que tu penses.
Les instructions préparées ou les requêtes paramétrées sont précompilées dans le serveur de base de données. Ainsi, lorsque les paramètres sont définis, aucune concaténation SQL n'est effectuée car la requête n'est plus une chaîne SQL. Un avantage supplémentaire est que le SGBDR met en cache la requête et que les appels suivants sont considérés comme le même SQL même lorsque les valeurs des paramètres varient, tandis qu'avec le SQL concaténé chaque fois que la requête est exécutée avec des valeurs différentes, la requête est différente et le SGBDR doit l'analyser , recréez le plan d'exécution, etc.
Imaginons à quoi ressemblerait une approche idéale de "désinfection, filtrage et codage".
La désinfection et le filtrage peuvent avoir un sens dans le contexte d'une application particulière, mais finalement ils se résument tous deux à dire "vous ne pouvez pas mettre ces données dans la base de données". Pour votre application, cela pourrait être une bonne idée, mais ce n'est pas quelque chose que vous pouvez recommander comme solution générale, car il y aura des applications qui devront pouvoir stocker des caractères arbitraires dans la base de données.
Cela laisse donc l'encodage. Vous pouvez commencer par avoir une fonction qui encode les chaînes en ajoutant des caractères d'échappement, afin de pouvoir les remplacer en vous-même. Étant donné que différentes bases de données nécessitent différents caractères d'échappement (dans certaines bases de données, les deux \'
et ''
sont des séquences d'échappement valides pour '
, mais pas dans d'autres), cette fonction doit être fournie par le fournisseur de la base de données.
Mais toutes les variables ne sont pas des chaînes. Parfois, vous devez remplacer un entier ou une date. Celles-ci sont représentées différemment des chaînes, vous avez donc besoin de différentes méthodes d'encodage (encore une fois, elles devraient être spécifiques au fournisseur de la base de données), et vous devez les remplacer dans la requête de différentes manières.
Alors peut-être que les choses seraient plus faciles si la base de données gérait également la substitution pour vous - elle sait déjà quels types la requête attend, et comment coder les données en toute sécurité, et comment les substituer dans votre requête en toute sécurité, donc vous n'avez pas à vous soucier de dans votre code.
À ce stade, nous venons de réinventer les requêtes paramétrées.
Et une fois les requêtes paramétrées, cela ouvre de nouvelles opportunités, telles que des optimisations de performances et une surveillance simplifiée.
L'encodage est difficile à faire correctement, et l'encodage effectué correctement ne se distingue pas du paramétrage.
Si vous aimez vraiment l'interpolation de chaînes comme moyen de construire des requêtes, il y a quelques langages (Scala et ES2015 me viennent à l'esprit) qui ont une interpolation de chaîne enfichable, donc làsontbibliothèques qui vous permettent d'écrire des requêtes paramétrées qui ressemblent à une interpolation de chaîne, mais sont à l'abri de l'injection SQL - donc dans la syntaxe ES2015:
import {sql} from 'cool-sql-library'
let result = sql`select *
from users
where user_id = ${user_id}
and password_hash = ${password_hash}`.execute()
console.log(result)
Je n'ai jamais utilisé SQL. Mais évidemment, vous entendez parler des problèmes que les gens ont, et les développeurs SQL ont eu des problèmes avec cette chose "injection SQL". Pendant longtemps, je n'ai pas pu le comprendre. Et puis j'ai réalisé que les gens créaient des instructions SQL, de véritables instructions source textuelles SQL, en concaténant des chaînes, dont certaines étaient entrées par un utilisateur. Et ma première pensée sur cette réalisation a été un choc. Choc total. J'ai pensé: Comment quelqu'un peut-il être aussi ridiculement stupide et créer des déclarations dans n'importe quel langage de programmation comme ça? Pour un développeur C, ou C++, ou Java, ou Swift développeur, c'est une folie totale.
Cela dit, il n'est pas très difficile d'écrire une fonction C qui prend une chaîne C comme argument et produit une chaîne différente qui ressemble exactement à un littéral de chaîne dans le code source C qui représente la même chaîne. Par exemple, cette fonction traduirait abc en "abc" et "abc" en "\" abc\"" et "\" abc\"" en "\" \\ "abc \\"\"". (Eh bien, si cela vous semble faux, c'est du HTML. C'était bien quand je l'ai tapé, mais pas quand il est affiché) Et une fois que la fonction C est écrite, il n'est pas difficile du tout de générer du code source C où le texte d'un champ de saisie fourni par l'utilisateur est transformé en un littéral de chaîne C. Ce n'est pas difficile à sécuriser. Pourquoi les développeurs SQL n'utiliseraient pas cette approche comme un moyen d'éviter les injections SQL me dépasse.
La "désinfection" est une approche totalement erronée. Le défaut fatal est qu'il rend certaines entrées utilisateur illégales. Vous vous retrouvez avec une base de données où un champ de texte générique ne peut pas contenir de texte comme; Drop Table ou tout ce que vous utiliseriez dans une injection SQL pour causer des dommages. Je trouve cela tout à fait inacceptable. Si une base de données stocke du texte, elle devrait pouvoir stocker n'importe quel texte. Et le défaut pratique est que le désinfectant ne semble pas faire les choses correctement :-(
Bien sûr, les requêtes paramétrées sont ce que tout programmeur utilisant un langage compilé attendrait. Cela rend la vie tellement plus facile: vous avez une entrée de chaîne, et vous ne vous souciez même pas de la traduire en une chaîne SQL, mais passez-la simplement en tant que paramètre, sans aucun risque que des caractères de cette chaîne n'endommagent.
Du point de vue d'un développeur utilisant des langages compilés, la désinfection est quelque chose qui ne me viendrait jamais à l'esprit. Le besoin de désinfection est fou. Les requêtes paramétrées sont la solution évidente au problème.
(J'ai trouvé la réponse de Josip intéressante. Il dit essentiellement qu'avec les requêtes paramétrées, vous pouvez arrêter toute attaque contre SQL, mais vous pouvez alors avoir du texte dans votre base de données qui est utilisé pour créer une injection JavaScript :-( Eh bien, nous avons à nouveau le même problème , et je ne sais pas si Javascript a une solution à cela.
Dans l'option 1, vous travaillez avec un ensemble d'entrée de taille = infini que vous essayez de mapper à une très grande taille de sortie. Dans l'option 2, vous avez limité votre entrée à tout ce que vous choisissez. En d'autres termes:
Selon d'autres réponses, il semble également y avoir des avantages en termes de performances à limiter votre portée loin de l'infini et vers quelque chose de gérable.
Un modèle mental utile de SQL (en particulier les dialectes modernes) est que chaque instruction ou requête SQL est un programme. Dans un programme exécutable binaire natif, les types de vulnérabilités de sécurité les plus dangereux sont les débordements où un attaquant peut écraser ou modifier le code du programme avec différentes instructions.
Une vulnérabilité d'injection SQL est isomorphe à un débordement de tampon dans un langage comme C. L'histoire a montré que les débordements de tampon sont extrêmement difficiles à empêcher - même le code extrêmement critique soumis à une révision ouverte contenait souvent de telles vulnérabilités.
Un aspect important de l'approche moderne pour résoudre les vulnérabilités de débordement est l'utilisation de mécanismes matériels et de système d'exploitation pour marquer des parties particulières de la mémoire comme non exécutables et pour marquer d'autres parties de la mémoire comme étant en lecture seule. (Voir l'article Wikipedia sur Protection de l'espace exécutable , par exemple.) De cette façon, même si un attaquant pouvait modifier les données, l'attaquant ne peut pas faire en sorte que les données injectées soient traitées comme du code.
Donc, si une vulnérabilité d'injection SQL équivaut à un débordement de tampon, quel est l'équivalent SQL d'un bit NX ou de pages mémoire en lecture seule? La réponse est: des instructions préparées , qui incluent des requêtes paramétrées et des mécanismes similaires pour les requêtes sans requête. L'instruction préparée est compilée avec certaines parties marquées en lecture seule, donc un attaquant ne peut pas modifier ces parties du programme, et d'autres parties marquées comme données non exécutables (les paramètres de l'instruction préparée), dans lesquelles l'attaquant pourrait injecter des données mais qui ne sera jamais traité comme un code de programme, éliminant ainsi la plupart des risques d'abus.
Certes, la désinfection des entrées utilisateur est bonne, mais pour être vraiment sûr, vous devez être paranoïaque (ou, de manière équivalente, penser comme un attaquant). Une surface de contrôle en dehors du texte du programme est le moyen de le faire, et les instructions préparées fournissent cette surface de contrôle pour SQL. Il n'est donc pas surprenant que les instructions préparées, et donc les requêtes paramétrées, soient l'approche recommandée par la grande majorité des professionnels de la sécurité.
J'ai déjà écrit à ce sujet ici: https://stackoverflow.com/questions/6786034/can-parameterized-statement-stop-all-sql-injection/33033576#33033576
Mais, pour faire simple:
Le fonctionnement des requêtes paramétrées est que la requête sqlQuery est envoyée en tant que requête et que la base de données sait exactement ce que fera cette requête, et alors seulement elle insérera le nom d'utilisateur et les mots de passe uniquement comme valeurs. Cela signifie qu'ils ne peuvent pas effectuer la requête, car la base de données sait déjà ce que fera la requête. Ainsi, dans ce cas, il recherchera un nom d'utilisateur de "Personne OR 1 = 1 '-" et un mot de passe vide, qui devrait apparaître faux.
Cependant, ce n'est pas une solution complète et la validation des entrées devra encore être effectuée, car cela n'affectera pas d'autres problèmes, tels que les attaques XSS, car vous pourriez toujours mettre javascript dans la base de données. Ensuite, si cela est lu sur une page, il l'affichera en tant que javascript normal, selon toute validation de sortie. Donc, la meilleure chose à faire est toujours d'utiliser la validation des entrées, mais en utilisant des requêtes paramétrées ou des procédures stockées pour arrêter les attaques SQL