PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Quanto è sicuro format() per le query dinamiche all'interno di una funzione?

Una parola di avvertimento :questo stile con SQL dinamico in SECURITY DEFINER le funzioni possono essere eleganti e convenienti. Ma non abusarne. Non annidare più livelli di funzioni in questo modo:

  • Lo stile è molto più soggetto a errori rispetto al semplice SQL.
  • Il cambio di contesto con SECURITY DEFINER ha un prezzo.
  • SQL dinamico con EXECUTE non è possibile salvare e riutilizzare i piani di query.
  • Nessuna "integrazione di funzioni".
  • E preferirei non usarlo affatto per grandi query su grandi tavoli. La raffinatezza aggiunta può essere una barriera alle prestazioni. Ad esempio:il parallelismo è disabilitato per i piani di query in questo modo.

Detto questo, la tua funzione sembra buona, non vedo alcun modo per l'iniezione SQL. format() si è dimostrato valido per concatenare e citare valori e identificatori per SQL dinamico. Al contrario, potresti rimuovere un po' di ridondanza per renderla più economica.

Parametri della funzione offset__i e limit__i sono integer . L'iniezione SQL è impossibile tramite numeri interi, non c'è davvero bisogno di virgolettarli (anche se SQL consente costanti stringa tra virgolette per LIMIT e OFFSET ). Quindi solo:

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

Inoltre, dopo aver verificato che ogni key__v è tra i nomi delle colonne legali e, sebbene siano tutti nomi di colonne legali senza virgolette, non è necessario eseguirlo tramite %I . Può essere solo %s

Preferirei usare text invece di varchar . Non è un grosso problema, ma text è il tipo di stringa "preferito".

Correlati:

COST 1 sembra troppo basso. Il manuale:

A meno che tu non lo sappia meglio, lascia COST al suo valore predefinito 100 .

Operazione basata su un singolo set invece di tutti i loop

L'intero ciclo può essere sostituito con un singolo SELECT dichiarazione. Dovrebbe essere notevolmente più veloce. Le assegnazioni sono relativamente costose in PL/pgSQL. In questo modo:

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<>violino qui

Più breve, più veloce e comunque sicuro contro SQLi.

Le virgolette vengono aggiunte solo dove necessario per la sintassi o per difendersi da SQL injection. Brucia solo per filtrare i valori. I nomi delle colonne e gli operatori vengono verificati rispetto all'elenco cablato delle opzioni consentite.

L'input è json invece di jsonb . L'ordine degli oggetti è mantenuto in json , così puoi determinare la sequenza delle colonne nel SELECT list (che è significativo) e WHERE condizioni (che è puramente cosmetico). La funzione ora osserva entrambi.

Output _result è ancora jsonb . Usando un OUT parametro invece della variabile. Questo è totalmente opzionale, solo per comodità. (Nessun RETURN esplicito dichiarazione richiesta.)

Nota l'uso strategico di concat() per ignorare silenziosamente NULL e l'operatore di concatenazione || in modo che NULL renda la stringa concatenata NULL. In questo modo, FROM , WHERE , LIMIT e OFFSET vengono inseriti solo dove necessario. Un SELECT istruzione funziona senza nessuno di questi. Un SELECT vuoto list (anche legale, ma suppongo indesiderato) risulta in un errore di sintassi. Tutto previsto.
Utilizzo di format() solo per WHERE filtri, per comodità e per citare i valori. Vedi:

La funzione non è STRICT più. _limit e _offset hanno il valore predefinito NULL , quindi solo il primo parametro _options è obbligatorio. _limit e _offset può essere NULL o omesso, quindi ciascuno viene rimosso dall'istruzione.

Usando text invece di varchar .

Fatto variabili costanti in realtà CONSTANT (principalmente per la documentazione).

A parte questo, la funzione fa quello che fa il tuo originale.