PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Dans quelle mesure format() est-il sécurisé pour les requêtes dynamiques à l'intérieur d'une fonction ?

Un avertissement  :ce style avec SQL dynamique dans SECURITY DEFINER les fonctions peuvent être élégantes et pratiques. Mais n'en abusez pas. N'imbriquez pas plusieurs niveaux de fonctions de cette manière :

  • Le style est beaucoup plus sujet aux erreurs que le SQL ordinaire.
  • Le changement de contexte avec SECURITY DEFINER a un prix.
  • SQL dynamique avec EXECUTE ne peut pas enregistrer et réutiliser les plans de requête.
  • Pas de "fonction inlining".
  • Et je préfère ne pas l'utiliser du tout pour de grosses requêtes sur de grandes tables. La sophistication supplémentaire peut être un obstacle aux performances. Comme :le parallélisme est désactivé pour les plans de requête de cette façon.

Cela dit, votre fonction a l'air bien, je ne vois aucun moyen d'injection SQL. format() s'est avéré efficace pour concaténer et citer des valeurs et des identifiants pour le SQL dynamique. Au contraire, vous pouvez supprimer certaines redondances pour le rendre moins cher.

Paramètres de la fonction offset__i et limit__i sont integer . L'injection SQL est impossible via des nombres entiers, il n'est vraiment pas nécessaire de les citer (même si SQL autorise les constantes de chaîne entre guillemets pour LIMIT et OFFSET ). Donc juste :

format(' OFFSET %s LIMIT %s', offset__i, limit__i)

Aussi, après avoir vérifié que chaque key__v fait partie de vos noms de colonne légaux - et bien que ce soient tous des noms de colonne légaux et sans guillemets - il n'est pas nécessaire de l'exécuter via %I . Peut être simplement %s

Je préfère utiliser text au lieu de varchar . Ce n'est pas grave, mais text est le type de chaîne "préféré".

Connexe :

COST 1 semble trop faible. Le manuel :

Sauf si vous savez mieux, laissez COST à sa valeur par défaut 100 .

Opération basée sur un seul ensemble au lieu de toutes les boucles

L'ensemble de la boucle peut être remplacé par un seul SELECT déclaration. Devrait être sensiblement plus rapide. Les affectations sont relativement chères en PL/pgSQL. Comme ceci :

CREATE OR REPLACE FUNCTION goods__list_json (_options json, _limit int = NULL, _offset int = NULL, OUT _result jsonb)
    RETURNS jsonb
    LANGUAGE plpgsql SECURITY DEFINER AS
$func$
DECLARE
   _tbl  CONSTANT text   := 'public.goods_full';
   _cols CONSTANT text[] := '{id, id__category, category, name, barcode, price, stock, sale, purchase}';   
   _oper CONSTANT text[] := '{<, >, <=, >=, =, <>, LIKE, "NOT LIKE", ILIKE, "NOT ILIKE", BETWEEN, "NOT BETWEEN"}';
   _sql           text;
BEGIN
   SELECT concat('SELECT jsonb_agg(t) FROM ('
           , 'SELECT ' || string_agg(t.col, ', '  ORDER BY ord) FILTER (WHERE t.arr->>0 = 'true')
                                               -- ORDER BY to preserve order of objects in input
           , ' FROM '  || _tbl
           , ' WHERE ' || string_agg (
                             CASE WHEN (t.arr->>1)::int BETWEEN  1 AND 10 THEN
                                format('%s %s %L'       , t.col, _oper[(arr->>1)::int], t.arr->>2)
                                  WHEN (t.arr->>1)::int BETWEEN 11 AND 12 THEN
                                format('%s %s %L AND %L', t.col, _oper[(arr->>1)::int], t.arr->>2, t.arr->>3)
                               -- ELSE NULL  -- = default - or raise exception for illegal operator index?
                             END
                           , ' AND '  ORDER BY ord) -- ORDER BY only cosmetic
           , ' OFFSET ' || _offset  -- SQLi-safe, no quotes required
           , ' LIMIT '  || _limit   -- SQLi-safe, no quotes required
           , ') t'
          )
   FROM   json_each(_options) WITH ORDINALITY t(col, arr, ord)
   WHERE  t.col = ANY(_cols)        -- only allowed column names - or raise exception for illegal column?
   INTO   _sql;

   IF _sql IS NULL THEN
      RAISE EXCEPTION 'Invalid input resulted in empty SQL string! Input: %', _options;
   END IF;
   
   RAISE NOTICE 'SQL: %', _sql;
   EXECUTE _sql INTO _result;
END
$func$;

db<>violon ici

Plus court, plus rapide et toujours sûr contre SQLi.

Les guillemets ne sont ajoutés que lorsque cela est nécessaire pour la syntaxe ou pour se défendre contre l'injection SQL. Brûle jusqu'aux valeurs de filtre uniquement. Les noms de colonne et les opérateurs sont vérifiés par rapport à la liste câblée des options autorisées.

L'entrée est json au lieu de jsonb . L'ordre des objets est conservé dans json , afin que vous puissiez déterminer la séquence des colonnes dans le SELECT list (qui a du sens) et WHERE conditions (ce qui est purement cosmétique). La fonction observe les deux maintenant.

Sortie _result est toujours jsonb . Utiliser un OUT paramètre au lieu de la variable. C'est totalement facultatif, juste pour plus de commodité. (Pas de RETURN explicite déclaration requise.)

Notez l'utilisation stratégique de concat() pour ignorer silencieusement NULL et l'opérateur de concaténation || de sorte que NULL rend la chaîne concaténée NULL. De cette façon, FROM , WHERE , LIMIT , et OFFSET ne sont insérés que là où c'est nécessaire. Un SELECT l'instruction fonctionne sans l'un ou l'autre. Un SELECT vide list (également légal, mais je suppose indésirable) entraîne une erreur de syntaxe. Tout est prévu.
Utilisation de format() uniquement pour WHERE filtres, pour plus de commodité et pour citer des valeurs. Voir :

La fonction n'est pas STRICT plus. _limit et _offset avoir la valeur par défaut NULL , donc seul le premier paramètre _options est requis. _limit et _offset peut être NULL ou omis, alors chacun est supprimé de l'instruction.

Utilisation de text au lieu de varchar .

Fait des variables constantes en fait CONSTANT (principalement pour la documentation).

À part cela, la fonction fait ce que fait votre original.