2016-06-17 2 views
2

У меня есть следующие функцииУсловный WHERE выражение в динамическом запросе

CREATE OR REPLACE FUNCTION match_custom_filter(filters text[], id text) 
     RETURNS boolean LANGUAGE plpgsql as $$ 
     DECLARE 
      r boolean; 
     BEGIN 
      execute format(
       'SELECT 1 FROM trackings t LEFT JOIN visitors v ON v.id = t.visitor_id 
       WHERE v.id = ''%s'' AND %s', 
       id, 
       array_to_string(filters, ') AND (')) 
      into r; 
      RETURN r; 
     END $$; 

select v.*, array_agg(g.name) as groups from visitors v join groups g on match_custom_filter(g.formatted_custom_filters, v.id) 
where v.id = 'cov4pisw00000sjctfyvwq126' 
group by v.id 

Это прекрасно работает, когда фильтры не являются пустыми. Но также возможно, что фильтр пуст, и в этом случае у меня будет свисающий И без правой стороны.

Ошибка:

ERROR: syntax error at end of input 
LINE 2: ...    WHERE v.id = 'cov4pisw00000sjctfyvwq126' AND 
                    ^
QUERY: SELECT 1 FROM trackings t LEFT JOIN visitors v ON v.id = t.visitor_id 
       WHERE v.id = 'cov4pisw00000sjctfyvwq126' AND 
CONTEXT: PL/pgSQL function match_custom_filter(text[],text) line 5 at EXECUTE statement 

Какой самый лучший способ справиться с этим?

ОБНОВЛЕНИЕ:

Пример того, как я генерировать массив строковых фильтров основаны от JSONB массив объектов фильтра

def build_condition(%{"filter" => filter, "field" => field, "value" => value}) when field in @default_values do 
    case filter do 
     "greater_than"  -> "#{field} > #{value}" 
     "less_than"   -> "#{field} < #{value}" 
     "is"    -> "#{field} = '#{value}'" 
     "is_not"   -> "#{field} <> '#{value}'" 
     .. 
+0

Может фильтр использовать любой оператор или это равенство? –

+0

Когда вы говорите «пусто», вы имеете в виду пустую строку '' '',' NULL' или или? – Patrick

+0

Операторы @ClodoaldoNeto, подобные этим: '# {field}> # {value}', '" # {field} ILIKE '# {value}' || '%' "и' "# {field} Tarlen

ответ

3

Во-первых, предупреждение. То, что вы делаете здесь, дает вам встроенную инъекцию sql-proc. Я настоятельно рекомендую вам пересмотреть, чтобы вы могли правильно параметризовать.

Теперь, говоря об этом, очевидным вариантом является объявление текстовой переменной, а затем ее предварительная обработка.

В вашем DECLARE блоке вы добавляете:

filterstring text; 

то в вашем теле, вы добавляете:

filterstring := array_to_string(filters, ') AND (')) 
IF filterstring = '' or filterstring is null THEN 
    filterstring := 'TRUE'; 
END IF; 

Затем вы используете filterstring вместо array_to_string вызова в format() вызова.

Обратите внимание, что каждый раз, когда вы собираете запрос в любом месте по строковой интерполяции, у вас есть возможность SQL-инъекции.

Для защиты от инъекций SQL вам нужно немного пересмотреть свой подход. Ваш лучший вариант - не использовать формат() для вашего запроса, насколько это возможно. Таким образом:

execute 'SELECT 1 FROM trackings t 
     LEFT JOIN visitors v ON v.id = t.visitor_id 
      WHERE v.id = $1' 
    USING id; 

Это вызывает планирование и заполнение значения в двух разных точках. Это хорошо работает в случае простого параметра. Однако это не работает в случае динамических фильтров.

Вместо передачи одномерного массива, вы можете передать двухмерный (массив nx3) с тремя элементами на строку. Это будут имя столбца, оператор и значение. Вы можете дезинфицировать имя столбца, передав его через quote_ident, и значение, пройдя его через quote_literal, но дезинфекция операторов, скорее всего, будет проблемой, поэтому моя рекомендация заключалась бы в их белом списке и исключении исключения, если оператор не найден. Что-то вроде:

DECLARE 
      ... 
      op TEXT; 
      allowed_ops TEXT[] := ARRAY['=', '<=', '>=']; 
    BEGIN 
     ... 
     IF not(op = ANY(allowed_ops)) THEN 
      RAISE EXCEPTION 'Illegal operator in function, %', op; 
     END IF; 
     ... 
    END; 

Это не будет легко, но это выполнимо.

+0

Простите мое невежество, я все еще очень новичок в DB. У меня создалось впечатление, что хранимые процедуры защищены от SQL-инъекции. Какие шаги я должен предпринять, чтобы защитить его должным образом, все еще имея возможность создавать динамические запросы? – Tarlen

+0

добавил, что информация. В вашем случае нет тривиальности, чтобы защитить себя, но, надеюсь, подсказки помогут. –

+0

На самом деле, я уже храню массив фильтров JSONB точно так же, как вы описываете. Я просто поддерживаю их в «конкатенированной» форме, как было предложено здесь: http://stackoverflow.com/questions/37844930/tagging-records-based-on-dynamic-attributes-and-filters-from-anothertable. В этом случае я все еще буду уязвим для SQL-инъекции, поскольку сопоставление объекта JSON с «значением отношения поля» осуществляется контролируемым образом. Обновлено сообщение с примером – Tarlen

2

Так как у вас есть свой filters in the form of a jsonb array, вы должны использовать его как параметр функции, а не text[]. Во-первых, это позволит вам защитить от SQL-инъекции.

CREATE OR REPLACE FUNCTION match_custom_filter(filters jsonb, id text) 
RETURNS boolean LANGUAGE plpgsql AS $$ 
DECLARE 
    f text; 
    r boolean; 
BEGIN 
    IF jsonb_array_length(filters) = 0 THEN 
     -- If no filters are specified then run a straight SQL query against trackings 
     PERFORM * FROM trackings WHERE visitor_id = quote_literal(id); 
     RETURN FOUND; 
    ELSE 
     -- Build the filters from the jsonb array 
     SELECT string_agg(
        -- Concatenate the parts from a single json object into a filter 
        quote_ident(j->>'field') || -- avoid SQL injection on column name 
        CASE j->>'type' 
         WHEN 'greater_than' THEN ' > ' 
         ... 
        END || 
        quote_literal(j->>'value'), -- avoid SQL injection on value 

        -- Aggregate individual filters with the AND operator 
        ' AND ') INTO f 
     FROM jsonb_array_elements(filters) j; 

     -- Run a dynamic query with the filters 
     EXECUTE format('SELECT true FROM trackings t 
         LEFT JOIN visitors v ON v.id = t.visitor_id 
         WHERE v.id = %L AND %s LIMIT 1', id, f) INTO r; 
     RETURN r; 
    END IF; 
END $$; 

Вы должны вызывать эту функцию переходящим в jsonb массиве, например, так:

SELECT v.*, array_agg(g.name) AS groups 
FROM visitors v JOIN groups g ON match_custom_filter(g.group->'filter', v.id) 
WHERE v.id = 'cov4pisw00000sjctfyvwq126' 
GROUP BY v.id; 
+0

Эй, я могу использовать массив фильтров JSONB, без проблем. Я выбрал этот подход только потому, что он был рекомендован в отношении простоты и производительности. Не могли бы вы привести пример того, как решить проблему с использованием объектов JSON, тем самым устраняя SQL-инъекцию? – Tarlen

+0

Ответ обновлен с помощью параметра функции 'jsonb'. – Patrick

Смежные вопросы