Pourquoi ne pas simplement renvoyer une erreur aux requêtes non paramétrées?

22

L'injection SQL est un problème de sécurité très grave, en grande partie parce qu'il est si facile de se tromper: la façon évidente et intuitive de créer une requête intégrant les entrées utilisateur vous rend vulnérable, et la bonne façon de l'atténuer nécessite que vous connaissiez les paramètres requêtes et injection SQL en premier.

Il me semble que le moyen évident de résoudre ce problème serait de fermer l'option évidente (mais erronée): corriger le moteur de base de données de sorte que toute requête reçue qui utilise des valeurs codées en dur dans sa clause WHERE au lieu de paramètres renvoie une belle description message d'erreur vous demandant d'utiliser des paramètres à la place. Cela nécessiterait évidemment une option de désactivation afin que des choses comme les requêtes ad hoc des outils d'administration puissent toujours s'exécuter facilement, mais elles devraient être activées par défaut.

Avoir cela arrêterait l'injection SQL à froid, presque du jour au lendemain, mais pour autant que je sache, aucun SGBDR ne le fait réellement. Y a-t-il une bonne raison pour laquelle non?

Mason Wheeler
la source
22
bad_ideas_sql = 'SELECT title FROM idea WHERE idea.status == "bad" AND idea.user == :mwheeler'aurait à la fois des valeurs codées en dur et paramétrées dans une seule requête - essayez d'attraper cela! Je pense qu'il existe des cas d'utilisation valables pour de telles requêtes mixtes.
amon
6
Que diriez-vous de sélectionner des enregistrements d'aujourd'huiSELECT * FROM jokes WHERE date > DATE_SUB(NOW(), INTERVAL 1 DAY) ORDER BY score DESC;
Jaydee
10
@MasonWheeler désolé, je voulais dire "essayez de permettre cela". Notez qu'il est parfaitement paramétré et ne souffre pas d'injection SQL. Cependant, le pilote de base de données ne peut pas dire si le littéral "bad"est vraiment littéral ou s'il résulte d'une concaténation de chaînes. Les deux solutions que je vois sont soit de se débarrasser de SQL et d'autres DSL intégrés aux chaînes (oui s'il vous plaît), soit de promouvoir des langages où la concaténation de chaînes est plus ennuyeuse que d'utiliser des requêtes paramétrées (umm, no).
amon
4
et comment le SGBDR pourrait-il détecter s’il le fallait? Cela rendrait impossible du jour au lendemain l'accès au SGBDR à l'aide d'une invite SQL interactive ... Vous ne seriez plus en mesure d'entrer des commandes DDL ou DML à l'aide d'un outil.
jwenting
8
Dans un sens, vous pouvez le faire: ne construisez pas du tout de requêtes SQL au moment de l'exécution, utilisez plutôt un ORM ou une autre couche d'abstraction qui vous évite d'avoir à construire des requêtes SQL. ORM ne possède pas les fonctionnalités dont vous avez besoin? Alors SQL est un langage destiné aux personnes qui veulent écrire du SQL, c'est pourquoi dans l'ensemble il leur permet d'écrire du SQL. Le problème fondamental est que la génération dynamique de code est plus difficile qu'il n'y paraît, mais les gens veulent quand même le faire et ne seront pas satisfaits des produits qui ne les autorisent pas.
Steve Jessop

Réponses:

45

Il y a trop de cas où l'utilisation d'un littéral est la bonne approche.

Du point de vue des performances, il y a des moments où vous voulez des littéraux dans vos requêtes. Imaginez que j'ai un traqueur de bogues où une fois qu'il sera suffisamment gros pour se soucier des performances, je m'attends à ce que 70% des bogues du système soient "fermés", 20% seront "ouverts", 5% seront "actifs" et 5 % aura un autre statut. Je peux raisonnablement vouloir que la requête qui renvoie tous les bogues actifs soit

SELECT *
  FROM bug
 WHERE status = 'active'

plutôt que de passer en statustant que variable de liaison. Je veux un plan de requête différent en fonction de la valeur transmise pour status- je voudrais faire une analyse de table pour retourner les bogues fermés et une analyse d'index sur lestatuspour renvoyer les prêts actifs. Désormais, différentes bases de données et différentes versions ont des approches différentes pour (avec plus ou moins de succès) permettre à la même requête d'utiliser un plan de requête différent en fonction de la valeur de la variable de liaison. Mais cela a tendance à introduire une quantité décente de complexité qui doit être gérée pour équilibrer la décision de déranger l'analyse d'une requête ou de réutiliser un plan existant pour une nouvelle valeur de variable de liaison. Pour un développeur, il peut être judicieux de gérer cette complexité. Ou il peut être judicieux de forcer un chemin différent lorsque j'ai plus d'informations sur l'aspect de mes données que l'optimiseur.

Du point de vue de la complexité du code, il y a aussi de nombreuses fois qu'il est parfaitement logique d'avoir des littéraux dans les instructions SQL. Par exemple, si vous avez une zip_codecolonne qui a un code postal à 5 ​​caractères et a parfois 4 chiffres supplémentaires, il est parfaitement logique de faire quelque chose comme

SELECT substr( zip_code, 1, 5 ) zip,
       substr( zip_code, 7, 4 ) plus_four

plutôt que de passer 4 paramètres distincts pour les valeurs numériques. Ce ne sont pas des choses qui changeront jamais, donc les rendre liées aux variables ne fait que rendre le code potentiellement plus difficile à lire et créer le potentiel que quelqu'un liera les paramètres dans le mauvais ordre et se retrouve avec un bogue.

Justin Cave
la source
12

L'injection SQL se produit lorsqu'une requête est créée en concaténant du texte à partir d'une source non approuvée et non validée avec d'autres parties d'une requête. Bien qu'une telle chose se produise le plus souvent avec des littéraux de chaîne, ce ne serait pas la seule façon de se produire. Une requête pour des valeurs numériques peut prendre une chaîne saisie par l' utilisateur (qui est supposé pour ne contenir que des chiffres) et concaténer avec d' autres matériaux pour former une requête sans les guillemets normalement associés à littéraux de chaîne; le code qui fait trop confiance à la validation côté client peut avoir des choses comme les noms de champ proviennent d'une chaîne de requête HTML. Il n'y a aucun moyen que le code regardant une chaîne de requête SQL puisse voir comment elle a été assemblée.

Ce qui est important n'est pas de savoir si une instruction SQL contient des littéraux de chaîne, mais plutôt si une chaîne contient des séquences de caractères provenant de sources non fiables , et la validation pour cela serait mieux gérée dans la bibliothèque qui construit les requêtes. Il n'y a généralement aucun moyen en C # d'écrire du code qui autorisera un littéral de chaîne mais ne permettra pas d'autres types d'expression de chaîne, mais on pourrait avoir une règle de pratiques de codage qui nécessite que les requêtes soient construites à l'aide d'une classe de construction de requêtes plutôt que la concaténation de chaînes et toute personne transmettant une chaîne non littérale au générateur de requêtes doit justifier une telle action.

supercat
la source
1
Comme approximation de "est-ce un littéral", vous pouvez vérifier si la chaîne est interne.
CodesInChaos
1
@CodesInChaos: Vrai, et un tel test peut être suffisamment précis à cet effet, à condition que toute personne ayant une raison de générer une chaîne à l'exécution utilise une méthode qui accepte une chaîne non littérale plutôt que d'interner la chaîne générée à l'exécution et d'utiliser cela (donner un nom différent à la méthode de chaîne non littérale faciliterait l'inspection de toutes les utilisations par les réviseurs de code).
supercat
Notez que bien qu'il n'y ait aucun moyen de le faire en C #, certains autres langages ont des fonctionnalités qui le rendent possible (par exemple le module de chaîne corrompue de Perl).
Jules
Plus succinctement, il s'agit d'un problème client , pas d'un problème serveur.
Blrfl
7
SELECT count(ID)
FROM posts
WHERE deleted = false

Si vous voulez mettre les résultats de ceux-ci dans le pied de page de votre forum, vous devrez ajouter un paramètre factice juste pour dire faux à chaque fois. Ou le programmeur web naïf cherche comment désactiver cet avertissement et continue ensuite.

Vous pouvez maintenant dire que vous ajouteriez une exception pour les énumérations, mais cela ouvre à nouveau le trou (bien que plus petit). Sans oublier que les gens doivent d'abord être éduqués pour ne pas les utiliser varchars.

Le vrai problème de l'injection est la construction par programme de la chaîne de requête. La solution pour cela est un mécanisme de procédure stockée et imposant son utilisation ou une liste blanche de requêtes autorisées.

monstre à cliquet
la source
2
Si votre solution pour "il est trop facile d'oublier - ou de ne pas savoir en premier lieu - d'utiliser des requêtes paramétrées" est "de faire en sorte que tout le monde se souvienne - et sache en premier lieu - d'utiliser des proc stockés", alors vous 'manque tout le point de la question.
Mason Wheeler
5
J'ai vu l'injection SQL via des procédures stockées dans mon travail. Il s'avère que rendre obligatoires les procédures stockées pour tout est MAUVAIS. Il y a toujours 0,5% de vraies requêtes dynamiques (vous ne pouvez pas paramétrer une clause where entière, encore moins une jointure de table).
Joshua
Dans l'exemple de cette réponse, vous pouvez remplacer deleted = falsepar NOT deleted, ce qui évite le littéral. Mais le point est valable en général.
psmears
5

TL; DR : Il faudrait restreindre tous les littéraux, pas seulement ceux des WHEREclauses. Pour des raisons pour lesquelles ils ne le font pas, il permet à la base de données de rester découplée des autres systèmes.

Premièrement, votre prémisse est défectueuse. Vous souhaitez restreindre uniquement les WHEREclauses, mais ce n'est pas le seul endroit où l'entrée utilisateur peut aller. Par exemple,

SELECT
    COUNT(CASE WHEN item_type = 'blender' THEN 1 END) as type1_count,
    COUNT(CASE WHEN item_type = 'television' THEN 1 END) AS type2_count)
FROM item

Ceci est également vulnérable à l'injection SQL:

SELECT
    COUNT(CASE WHEN item_type = 'blender' THEN 1 END) FROM item; DROP TABLE user_info; SELECT CASE(WHEN item_type = 'blender' THEN 1 END) as type1_count,
    COUNT(CASE WHEN item_type = 'television' THEN 1 END) AS type2_count)
FROM item

Vous ne pouvez donc pas simplement restreindre les littéraux dans la WHEREclause. Vous devez restreindre tous les littéraux.

Il nous reste maintenant la question: "Pourquoi autoriser les littéraux?" Gardez cela à l'esprit: bien que les bases de données relationnelles soient utilisées sous une application écrite dans une autre langue un pourcentage important du temps, il n'est pas nécessaire d' utiliser le code d'application pour utiliser la base de données. Et ici, nous avons une réponse: vous avez besoin de littéraux pour écrire du code. La seule autre alternative serait d'exiger que tout le code soit écrit dans une langue indépendante de la base de données. Les avoir vous donne donc la possibilité d'écrire du "code" (SQL) directement dans la base de données. Il s'agit d'un découplage précieux, et il serait impossible sans littéraux. (Essayez d'écrire dans votre langue préférée parfois sans littéraux. Je suis sûr que vous pouvez imaginer à quel point cela serait difficile.)

À titre d'exemple courant, les littéraux sont souvent utilisés dans la population des tables de liste de valeurs / de recherche:

CREATE TABLE user_roles (role_id INTEGER, role_name VARCHAR(50));
INSERT INTO user_roles (1, 'normal');
INSERT INTO user_roles (2, 'admin');
INSERT INTO user_roles (3, 'banned');

Sans eux, vous auriez besoin d'écrire du code dans un autre langage de programmation juste pour remplir ce tableau. La possibilité de le faire directement en SQL est précieuse .

Il nous reste alors une autre question: pourquoi les bibliothèques clientes du langage de programmation ne le font-elles pas alors? Et ici, nous avons une réponse très simple: ils auraient ré-implémenté l'intégralité de l'analyseur de base de données pour chaque version prise en charge de la base de données . Pourquoi? Parce qu'il n'y a pas d'autre moyen de garantir que vous avez trouvé chaque littéral. Les expressions régulières ne suffisent pas. Par exemple: cela contient 4 littéraux distincts dans PostgreSQL:

SELECT $lit1$I'm a literal$lit1$||$lit2$I'm another literal $$ with nested string delimiters$$ $lit2$||'I''m ANOTHER literal'||$$I'm the last literal$$;

Essayer de le faire serait un cauchemar de maintenance, d'autant plus que la syntaxe valide change souvent entre les principales versions des bases de données.

jpmc26
la source