PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Wie sicher ist format() für dynamische Abfragen innerhalb einer Funktion?

Ein Wort der Warnung :dieser Stil mit dynamischem SQL in SECURITY DEFINER Funktionen können elegant und bequem sein. Aber übertreiben Sie es nicht. Verschachteln Sie nicht mehrere Ebenen von Funktionen auf diese Weise:

  • Der Stil ist viel fehleranfälliger als einfaches SQL.
  • Der Kontextwechsel mit SECURITY DEFINER hat ein Preisschild.
  • Dynamisches SQL mit EXECUTE Abfragepläne können nicht gespeichert und wiederverwendet werden.
  • Kein "Funktions-Inlining".
  • Und ich würde es lieber überhaupt nicht für große Abfragen in großen Tabellen verwenden. Die zusätzliche Raffinesse kann ein Leistungshindernis sein. Zum Beispiel:Auf diese Weise wird die Parallelität für Abfragepläne deaktiviert.

Das heißt, Ihre Funktion sieht gut aus, ich sehe keine Möglichkeit für eine SQL-Injektion. format() hat sich bewährt, um Werte und Bezeichner für dynamisches SQL zu verketten und zu zitieren. Im Gegenteil, Sie könnten einige Redundanzen entfernen, um es billiger zu machen.

Funktionsparameter offset__i und limit__i sind integer . SQL-Injection ist durch Integer-Zahlen nicht möglich, es besteht wirklich keine Notwendigkeit, sie in Anführungszeichen zu setzen (obwohl SQL in Anführungszeichen gesetzte String-Konstanten für LIMIT zulässt und OFFSET ). Also einfach:

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

Auch nach der Überprüfung, dass jeder key__v zu Ihren zulässigen Spaltennamen gehört - und obwohl dies alles zulässige Spaltennamen ohne Anführungszeichen sind - besteht keine Notwendigkeit, sie durch %I laufen zu lassen . Kann nur %s sein

Ich würde lieber text verwenden statt varchar . Keine große Sache, aber text ist der "bevorzugte" Zeichenfolgentyp.

Verwandte:

COST 1 scheint zu niedrig. Das Handbuch:

Wenn Sie es nicht besser wissen, lassen Sie COST standardmäßig 100 .

Einzelsatz-basierter Betrieb statt Schleifen

Die gesamte Schleife kann durch ein einzelnes SELECT ersetzt werden Aussage. Sollte deutlich schneller sein. Zuweisungen sind in PL/pgSQL vergleichsweise teuer. So:

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<>fiddle hier

Kürzer, schneller und trotzdem sicher gegen SQLi.

Anführungszeichen werden nur hinzugefügt, wenn dies für die Syntax oder zum Schutz vor SQL-Injection erforderlich ist. Brennt nur auf Filterwerte. Spaltennamen und Operatoren werden anhand der fest verdrahteten Liste zulässiger Optionen überprüft.

Die Eingabe ist json statt jsonb . Die Reihenfolge der Objekte wird in json beibehalten , damit Sie die Reihenfolge der Spalten im SELECT bestimmen können list (was sinnvoll ist) und WHERE Bedingungen (was rein kosmetischer Natur ist). Die Funktion beachtet jetzt beides.

_result ausgeben ist immer noch jsonb . Mit einem OUT Parameter statt Variable. Das ist völlig optional, nur der Bequemlichkeit halber. (Kein explizites RETURN Angabe erforderlich.)

Beachten Sie die strategische Verwendung von concat() um NULL und den Verkettungsoperator || stillschweigend zu ignorieren sodass NULL die verkettete Zeichenfolge zu NULL macht. Auf diese Weise FROM , WHERE , LIMIT , und OFFSET werden nur dort eingefügt, wo sie benötigt werden. Ein SELECT Anweisung funktioniert ohne eines von beiden. Ein leeres SELECT list (ebenfalls legal, aber ich nehme an, dass es unerwünscht ist) führt zu einem Syntaxfehler. Alles beabsichtigt.
Mit format() nur für WHERE Filter, zur Bequemlichkeit und um Werte zu zitieren. Siehe:

Die Funktion ist nicht STRICT mehr. _limit und _offset haben den Standardwert NULL , also nur der erste Parameter _options ist nötig. _limit und _offset kann NULL sein oder weggelassen werden, dann wird jedes aus der Anweisung entfernt.

Verwendung von text statt varchar .

Konstante Variablen tatsächlich CONSTANT gemacht (hauptsächlich zur Dokumentation).

Ansonsten tut die Funktion das, was Ihr Original tut.