sql >> Base de Datos >  >> RDS >> PostgreSQL

¿Qué tan seguro es format() para consultas dinámicas dentro de una función?

Una palabra de advertencia :este estilo con SQL dinámico en SECURITY DEFINER Las funciones pueden ser elegantes y convenientes. Pero no lo abuses. No anide múltiples niveles de funciones de esta manera:

  • El estilo es mucho más propenso a errores que SQL simple.
  • El cambio de contexto con SECURITY DEFINER tiene una etiqueta de precio.
  • SQL dinámico con EXECUTE no se pueden guardar y reutilizar planes de consulta.
  • Sin "función en línea".
  • Y prefiero no usarlo para consultas grandes en tablas grandes. La sofisticación añadida puede ser una barrera de rendimiento. Me gusta:el paralelismo está deshabilitado para los planes de consulta de esta manera.

Dicho esto, su función se ve bien, no veo forma de inyección de SQL. formato() ha demostrado ser bueno para concatenar y citar valores e identificadores para SQL dinámico. Por el contrario, podrías eliminar algo de redundancia para hacerlo más económico.

Parámetros de función offset__i y limit__i son integer . La inyección de SQL es imposible a través de números enteros, realmente no hay necesidad de entrecomillarlos (aunque SQL permite constantes de cadena entrecomilladas para LIMIT y OFFSET ). Así que solo:

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

Además, después de verificar que cada key__v se encuentra entre los nombres de columna legales, y aunque todos son nombres de columna legales sin comillas, no es necesario ejecutarlo a través de %I . Solo puede ser %s

Prefiero usar text en lugar de varchar . No es gran cosa, pero text es el tipo de cadena "preferido".

Relacionado:

COST 1 parece demasiado bajo. El manual:

A menos que sepa mejor, deje COST en su valor predeterminado 100 .

Operación basada en conjuntos únicos en lugar de todos los bucles

Todo el bucle se puede reemplazar con un solo SELECT declaración. Debería ser notablemente más rápido. Las asignaciones son comparativamente caras en PL/pgSQL. Así:

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 aquí

Más corto, más rápido y seguro contra SQLi.

Las comillas solo se agregan cuando son necesarias para la sintaxis o para defenderse contra la inyección de SQL. Se reduce a valores de filtro únicamente. Los nombres de las columnas y los operadores se verifican con la lista fija de opciones permitidas.

La entrada es json en lugar de jsonb . El orden de los objetos se conserva en json , para que pueda determinar la secuencia de columnas en el SELECT list (que es significativo) y WHERE condiciones (que es puramente cosmético). La función observa ambos ahora.

Salida _result sigue siendo jsonb . Usando un OUT parámetro en lugar de la variable. Eso es totalmente opcional, solo por conveniencia. (Sin RETURN explícito declaración requerida.)

Tenga en cuenta el uso estratégico de concat() para ignorar silenciosamente NULL y el operador de concatenación || de modo que NULL convierte la cadena concatenada en NULL. De esta manera, FROM , WHERE , LIMIT y OFFSET solo se insertan donde es necesario. Un SELECT declaración funciona sin ninguno de los dos. Un SELECT vacío list (también legal, pero supongo que no deseado) da como resultado un error de sintaxis. Todo intencionado.
Usando format() solo para WHERE filtros, por comodidad y para cotizar valores. Ver:

La función no es STRICT más. _limit y _offset tiene valor predeterminado NULL , por lo que solo el primer parámetro _options es requerido. _limit y _offset puede ser NULL u omitido, luego cada uno se elimina de la instrucción.

Usando text en lugar de varchar .

Hizo variables constantes en realidad CONSTANT (principalmente para documentación).

Aparte de eso, la función hace lo que hace su original.