Les instructions préparées sont-elles réellement 100% sûres contre l'injection SQL, en supposant que tous les paramètres fournis par l'utilisateur sont passés en tant que paramètres liés à la requête?
Chaque fois que je vois des gens utiliser l'ancien mysql_
fonctions sur StackOverflow (ce qui est malheureusement trop souvent) Je dis généralement aux gens que les instructions préparées sont le Chuck Norris (ou Jon Skeet) des mesures de sécurité des injections SQL.
Cependant, je n'ai jamais vu de documentation indiquant catégoriquement "c'est 100% sûr" . Ma compréhension d'eux est qu'ils séparent le langage de requête et les paramètres jusqu'à la porte d'entrée du serveur, qui les traite ensuite comme des entités distinctes.
Suis-je correct dans cette hypothèse?
Garantie de 100% à l'abri de l'injection SQL? Je ne vais pas l'obtenir (de moi).
En principe, votre base de données (ou bibliothèque dans votre langue qui interagit avec la base de données) pourrait implémenter des instructions préparées avec des paramètres liés d'une manière dangereuse susceptible d'une sorte d'attaque avancée, par exemple en exploitant les débordements de tampon ou en ayant des caractères de terminaison nulle dans l'utilisateur - fourni des chaînes, etc. (Vous pourriez faire valoir que ces types d'attaques ne devraient pas être appelés injection SQL car ils sont fondamentalement différents; mais ce n'est que de la sémantique).
Je n'ai jamais entendu parler de l'une de ces attaques contre des déclarations préparées sur de vraies bases de données sur le terrain et je suggère fortement d'utiliser des paramètres liés pour empêcher l'injection SQL. Sans paramètres liés ni assainissement d'entrée, il est trivial de faire une injection SQL. Avec seulement l'assainissement d'entrée, il est souvent possible de trouver une faille obscure autour de l'assainissement.
Avec des paramètres liés, votre plan d'exécution de requête SQL est déterminé à l'avance sans dépendre des entrées de l'utilisateur, ce qui devrait rendre l'injection SQL impossible (car les guillemets insérés, les symboles de commentaire, etc. ne sont insérés que dans l'instruction SQL déjà compilée).
Le seul argument contre l'utilisation d'instructions préparées est que vous souhaitez que votre base de données optimise vos plans d'exécution en fonction de la requête réelle. La plupart des bases de données, lorsqu'elles reçoivent la requête complète, sont suffisamment intelligentes pour faire un plan d'exécution optimal; par exemple, si la requête renvoie un grand pourcentage de la table, elle voudra parcourir toute la table pour trouver des correspondances; alors que si cela ne permet d'obtenir que quelques enregistrements, vous pouvez effectuer une recherche basée sur un index [1] .
EDIT: Répondre à deux critiques (qui sont un peu trop longues pour les commentaires):
Tout d'abord, comme d'autres l'ont fait remarquer, chaque base de données relationnelle prenant en charge les instructions préparées et les paramètres liés ne précompile pas nécessairement l'instruction préparée sans regarder la valeur des paramètres liés. De nombreuses bases de données le font habituellement, mais il est également possible pour les bases de données de regarder les valeurs des paramètres liés lors de la détermination du plan d'exécution. Ce n'est pas un problème, car la structure de l'instruction préparée avec des paramètres liés séparés, permet à la base de données de différencier facilement l'instruction SQL (y compris les mots clés SQL) des données des paramètres liés (où rien dans un paramètre lié ne sera interprété comme un mot clé SQL). Cela n'est pas possible lors de la construction d'instructions SQL à partir d'une concaténation de chaînes où les variables et les mots clés SQL seraient mélangés.
Deuxièmement, comme l'indique autre réponse , l'utilisation de paramètres liés lors de l'appel d'une instruction SQL à un moment donné dans un programme empêchera en toute sécurité l'injection SQL lors de cet appel de niveau supérieur. Cependant, si vous avez des vulnérabilités d'injection SQL ailleurs dans l'application (par exemple, dans les fonctions définies par l'utilisateur que vous avez stockées et exécutées dans votre base de données que vous avez mal écrites pour construire des requêtes SQL par concaténation de chaînes).
Par exemple, si dans votre application vous avez écrit un pseudo-code comme:
sql_stmt = "SELECT create_new_user(?, ?)"
params = (email_str, hashed_pw_str)
db_conn.execute_with_params(sql_stmt, params)
Il ne peut pas y avoir d'injection SQL lors de l'exécution de cette instruction SQL au niveau de l'application. Cependant, si la fonction de base de données définie par l'utilisateur a été écrite de manière non sécurisée (en utilisant la syntaxe PL/pgSQL):
CREATE FUNCTION create_new_user(email_str, hashed_pw_str) RETURNS VOID AS
$$
DECLARE
sql_str TEXT;
BEGIN
sql_str := 'INSERT INTO users VALUES (' || email_str || ', ' || hashed_pw_str || ');'
EXECUTE sql_str;
END;
$$
LANGUAGE plpgsql;
vous seriez alors vulnérable aux attaques par injection SQL, car il exécute une instruction SQL construite via la concaténation de chaînes qui mélange l'instruction SQL avec des chaînes contenant les valeurs des variables définies par l'utilisateur.
Cela dit, à moins que vous n'essayiez d'être dangereux (construction d'instructions SQL via la concaténation de chaînes), il serait plus naturel d'écrire le défini par l'utilisateur de manière sûre comme:
CREATE FUNCTION create_new_user(email_str, hashed_pw_str) RETURNS VOID AS
$$
BEGIN
INSERT INTO users VALUES (email_str, hashed_pw_str);
END;
$$
LANGUAGE plpgsql;
De plus, si vous avez vraiment ressenti le besoin de composer une instruction SQL à partir d'une chaîne dans une fonction définie par l'utilisateur, vous pouvez toujours séparer les variables de données de l'instruction SQL de la même manière que les paramètres stockés_procédures/liés, même au sein d'une fonction définie par l'utilisateur. Par exemple dans PL/pgSQL :
CREATE FUNCTION create_new_user(email_str, hashed_pw_str) RETURNS VOID AS
$$
DECLARE
sql_str TEXT;
BEGIN
sql_str := 'INSERT INTO users VALUES($1, $2)'
EXECUTE sql_str USING email_str, hashed_pw_str;
END;
$$
LANGUAGE plpgsql;
Ainsi, l'utilisation d'instructions préparées est à l'abri de l'injection SQL, tant que vous ne faites pas simplement des choses dangereuses ailleurs (c'est-à-dire la construction d'instructions SQL par concaténation de chaînes).
100% sûr? Pas même près. Les paramètres liés (préparés au niveau des instructions ou autrement) peuvent empêcher efficacement, à 100%, une classe de vulnérabilité d'injection SQL (en supposant qu'il n'y a pas de bogue db et une implémentation saine ). En aucun cas, ils n'empêchent d'autres classes. Notez que PostgreSQL (ma base de données de choix) a une option pour lier les paramètres aux instructions ad hoc, ce qui permet d'économiser un aller-retour concernant les instructions préparées si vous n'en avez pas besoin de certaines fonctionnalités.
Vous devez comprendre que de nombreuses grandes bases de données complexes sont des programmes en soi. La complexité de ces programmes varie beaucoup, et l'injection SQL est quelque chose qui doit être recherché dans les routines de programmation. De telles routines comprennent des déclencheurs, des fonctions définies par l'utilisateur, des procédures stockées, etc. Il n'est pas toujours évident de savoir comment ces choses interagissent à partir d'un niveau d'application, car de nombreuses bonnes bases de données fournissent un certain degré d'abstraction entre le niveau d'accès à l'application et le niveau de stockage.
Avec des paramètres liés, l'arbre de requête est analysé, puis, dans PostgreSQL au moins, les données sont examinées afin de planifier. Le plan est exécuté. Avec les instructions préparées, le plan est enregistré afin que vous puissiez réexécuter le même plan avec différentes données encore et encore (cela peut ou non être ce que vous voulez). Mais le fait est qu'avec des paramètres liés, un paramètre ne peut rien injecter dans l'arbre d'analyse. Donc, cette classe de problème d'injection SQL est correctement prise en charge.
Mais maintenant, nous devons consigner qui écrit quoi dans une table, nous ajoutons donc des déclencheurs et des fonctions définies par l'utilisateur pour encapsuler la logique de ces déclencheurs. Cela pose de nouveaux problèmes. Si vous avez du SQL dynamique dans ceux-ci, vous devez vous soucier de l'injection SQL. Les tables dans lesquelles ils écrivent peuvent avoir des déclencheurs qui leur sont propres, etc. De même, un appel de fonction peut appeler une autre requête qui peut appeler un autre appel de fonction, etc. Chacun de ces éléments est planifié indépendamment de l'arbre principal.
Cela signifie que si j'exécute une requête avec un paramètre lié comme foo'; drop user postgres; --
alors il ne peut pas impliquer directement l'arbre de requête de niveau supérieur et l'amener à ajouter une autre commande pour supprimer l'utilisateur postgres. Cependant, si cette requête appelle une autre fonction directement ou non, il est possible que quelque part en aval, une fonction soit vulnérable et l'utilisateur postgres soit supprimé. Les paramètres liés n'offraient aucune protection aux requêtes secondaires. Ces requêtes secondaires doivent s'assurer qu'elles utilisent également les paramètres liés dans la mesure du possible et, dans le cas contraire, elles devront utiliser des routines de devis appropriées.
Le trou du lapin est profond.
BTW pour une question sur Stack Overflow où ce problème est apparent, voir https://stackoverflow.com/questions/37878426/conditional-where-expression-in-dynamic-query/37878574#37878574
Également une version plus problématique (en raison de la limitation des déclarations d'utilitaires) sur https://stackoverflow.com/questions/38016764/perform-create-index-in-plpgsql-doesnt-run/38021245#38021245