PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Quão seguro é o format() para consultas dinâmicas dentro de uma função?


Uma palavra de aviso :este estilo com SQL dinâmico em SECURITY DEFINER funções podem ser elegantes e convenientes. Mas não abuse. Não aninhe vários níveis de funções desta forma:
  • O estilo é muito mais propenso a erros do que o SQL simples.
  • A alternância de contexto com SECURITY DEFINER tem uma etiqueta de preço.
  • SQL dinâmico com EXECUTE não é possível salvar e reutilizar planos de consulta.
  • Sem "função embutida".
  • E eu prefiro não usá-lo para grandes consultas em grandes tabelas. A sofisticação adicional pode ser uma barreira de desempenho. Como:o paralelismo é desabilitado para planos de consulta dessa maneira.

Dito isso, sua função parece boa, não vejo nenhuma maneira de injeção de SQL. formato() é comprovadamente bom para concatenar e citar valores e identificadores para SQL dinâmico. Pelo contrário, você pode remover alguma redundância para torná-lo mais barato.

Parâmetros de função offset__i e limit__i são integer . A injeção de SQL é impossível através de números inteiros, não há necessidade de citá-los (mesmo que o SQL permita constantes de string entre aspas para LIMIT e OFFSET ). Então apenas:
format(' OFFSET %s LIMIT %s', offset__i, limit__i)

Além disso, após verificar se cada key__v está entre seus nomes de colunas legais - e embora todos sejam nomes de colunas legais e sem aspas - não há necessidade de executá-lo através de %I . Pode ser apenas %s

Prefiro usar text em vez de varchar . Não é grande coisa, mas text é o tipo de string "preferido".

Relacionado:

COST 1 parece muito baixo. O manual:

A menos que você saiba melhor, deixe COST em seu padrão 100 .

Operação baseada em conjunto único em vez de todos os loops


Todo o loop pode ser substituído por um único SELECT declaração. Deve ser visivelmente mais rápido. As atribuições são comparativamente caras em PL/pgSQL. Assim:
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 aqui

Mais curto, mais rápido e ainda seguro contra SQLi.

As aspas são adicionadas apenas quando necessário para sintaxe ou para se defender contra injeção de SQL. Queima apenas para filtrar valores. Os nomes e operadores das colunas são verificados em relação à lista de opções permitidas por fio.

A entrada é json em vez de jsonb . A ordem dos objetos é preservada em json , para que você possa determinar a sequência de colunas no SELECT list (que é significativo) e WHERE condições (que é puramente cosmético). A função observa ambos agora.

Saída _result ainda é jsonb . Usando um OUT parâmetro em vez da variável. Isso é totalmente opcional, apenas por conveniência. (Sem RETURN explícito declaração necessária.)

Observe o uso estratégico de concat() para ignorar silenciosamente NULL e o operador de concatenação || para que NULL torne a string concatenada NULL. Desta forma, FROM , WHERE , LIMIT e OFFSET são inseridos apenas onde necessário. Um SELECT declaração funciona sem nenhum deles. Um SELECT vazio list (também legal, mas suponho que indesejado) resulta em um erro de sintaxe. Tudo pretendido.
Usando format() apenas para WHERE filtros, por conveniência e para cotar valores. Ver:

A função não é STRICT não mais. _limit e _offset tem valor padrão NULL , portanto, apenas o primeiro parâmetro _options É necessário. _limit e _offset pode ser NULL ou omitido, então cada um é removido da instrução.

Usando text em vez de varchar .

Variáveis ​​constantes feitas na verdade CONSTANT (principalmente para documentação).

Fora isso, a função faz o que o original faz.