Как использовать exec_query с динамическим SQL

#sql #ruby-on-rails #postgresql

Вопрос:

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

 SELECT JSON_AGG(agg_date)
FROM (
 SELECT t1.col1, t1.col2, t2.col1, t2.col2, t3.col3, t3.col4, t4.col7, t4.col8, t5.col5, t5.col6
 FROM t1
 JOIN t2 ON t1.id = t2.t1_id
 JOIN t3 ON t1.id = t3.t3_id
 JOIN t4 ON t2.is = t4.t2_id
 JOIN t5 ON t3.id = t5.t3_id
  
 WHERE t2.id IN ($1) AND t4.id = $2
) agg_data
 

это дает ошибку invalid input syntax for integer: '1,2,3,4,5'

И SELECT ... WHERE t.id = ANY($1) дает ERROR: malformed array literal: "1,2,3,4,5,6,7" DETAIL: Array value must start with "{" or dimension information.

Если я добавлю фигурные скобки вокруг переменной привязки, я получу invalid input syntax for integer: "$1"

Вот как я использую exec_query

 connection.exec_query(<<~EOQ, "-- CUSTOM SQL --", [[nil, array_of_ids], [nil, model_id]], prepare: true)
  SELECT ... WHERE t.id IN ($1)
EOQ
 

Я пробовал использовать обычную интерполяцию, но это приводит к ошибкам тормозного оператора при внедрении sql, поэтому я не могу использовать этот способ 🙁

Мы высоко ценим любую помощь в проведении этой проверки. И если exec_query это неправильный способ сделать это, я определенно готов попробовать другие вещи 😀

В моем классе я использую внутреннюю защиту от инъекций sql AR для поиска первых идентификаторов переменных привязки, затем выбираю идентификаторы и объединяю их в строку для sql-запроса. Я делаю то же самое для другой переменной привязки, нахожу объект и использую этот идентификатор. Просто в качестве дополнительной меры предосторожности. Таким образом, к тому времени, когда вводимые пользователем данные используются для запроса, они уже прошли AR. Это сканирование тормозника, которое выдает ошибку. У меня в понедельник встреча с нашей службой безопасности по этому поводу, но я хотел бы также проверить здесь 😀

Комментарии:

1. Можете ли вы показать нам фактический запрос? Трудно на самом деле дать достойный ответ, имея лишь часть картины? Это то, что вы можете создать с помощью интерфейса AR-запроса и Arel вместо строки?

2. Я не думаю, что AR будет такой же производительной, и мне действительно не нужна какая-то одна конкретная модель. Я обновил вопрос, чтобы показать основу того, что я пытаюсь сделать. Мне нужны данные json agg, и я не думаю, что AR сможет это сделать или так же эффективно, как необработанный sql

Ответ №1:

Позвольте Rails провести дезинфекцию за вас:

 ar = [1,2,8,9,100,800]

MyModel.where(id: ar)
 

ваша забота о внедрении sql предполагает, что ar является производным от пользовательского ввода. Это излишне, но, возможно, вы захотите убедиться, что это список целых чисел. ar = user_ar.map(amp;:to_i) .

 # with just Rails sanitization
ar = "; drop table users;" # sql injection

MyModel.where(id: ar)

# query is:
# SELECT `my_models`.* from `my_models` WHERE `my_models`.`id` = NULL;

# or
ar = [1,2,8,100,"; drop table users;"]

MyModel.where(id: ar)

# query is
# SELECT `my_models`.* from `my_models` WHERE `my_models`.`id` in (1,2,8,100);
 

Рельсы тебя накрыли!

Комментарии:

1. Я знаю о ARs, встроенном в предотвращение SQL — инъекций. Я обновлю вопрос, но у меня есть выбор, который, на мой взгляд, слишком сложен для AR. Кроме того, я думаю, что, поскольку мне не нужны реальные объекты AR из БД, я чувствую, что необработанный sql лучше с точки зрения производительности.

2. спасибо за предложение. Я использовал AR для сортировки входных данных 😛 Я получал коллекцию идентификаторов, находил коллекцию объектов AR, а затем объединял идентификаторы в строку для необработанного запроса. Я надеялся найти способ, которым мне не пришлось бы этого делать. Но у меня была встреча с моим сотрудником службы безопасности на работе, и они сообщили мне, что это было ложное срабатывание брейкмана из-за моей предварительной обработки. Спасибо, что подкрепили мой выбор 😀

Ответ №2:

С помощью Arel вы можете составить этот запрос следующим образом:

 class Aggregator

  def initialize(connection: ActiveRecord::Base.connection)
    @connection = connection
    @t1 = Arel::Table.new('t1')
    @t2 = Arel::Table.new('t2')
    @t3 = Arel::Table.new('t3')
    @t4 = Arel::Table.new('t4')
    @t5 = Arel::Table.new('t5')
    @columns = [
      :col1, 
      :col2, 
      @t2[:col1], 
      @t2[:col2], 
      @t3[:col3], 
      @t3[:col4], 
      @t4[:col7], 
      @t4[:col8], 
      @t5[:col5], 
      @t5[:col6]
    ]
  end
  
  def query(t2_ids:, t4_id:)
    agg_data = t1.project(*columns)
                 .where(
                   t2[:id].in(t2_ids)
                   .and(t4[:id].eq(t4_id))
                 ) 
                 .join(t2).on(t1[:id].eq(t2[:t1_id]))
                 .join(t3).on(t1[:id].eq(t3[:t1_id]))
                 .join(t4).on(t1[:id].eq(t4[:t1_id]))
                 .join(t5).on(t1[:id].eq(t5[:t1_id])) 
                 .as('agg_data')

    yield agg_data if block_given?
    t1.project('JSON_AGG(agg_data)')
      .from(agg_data)
  end

  def exec_query(t2_ids:, t4_id:)
    connection.exec_query(
      query(t2_ids: t2_ids, t4_id: t4_id),
      "-- CUSTOM SQL --"
    )
  end

  private

  attr_reader :connection, :t1, :t2, :t3, :t4, :t5, :columns

end
 

Конечно, было бы намного чище просто настроить некоторые модели так, чтобы вы могли это сделать t1.joins(:t2, :t3, :t4, ...) . Ваши опасения по поводу производительности довольно необоснованны, поскольку у ActiveRecord есть довольно много методов для запроса и получения необработанных результатов вместо экземпляров модели.

Использование переменных привязки для WHERE IN () условия несколько проблематично, так как вы должны использовать количество переменных привязки, соответствующее количеству элементов в списке:

 irb(main):118:0> T1.where(id: [1, 2, 3])
  T1 Load (0.2ms)  SELECT "t1s".* FROM "t1s" WHERE "t1s"."id" IN (?, ?, ?) /* loading for inspect */ LIMIT ?
 

Это означает, что вы должны заранее знать количество переменных привязки при подготовке запроса. В качестве хитрого обходного пути вы можете использовать некоторую творческую типизацию, чтобы заставить Postgres разделить строку, разделенную запятыми, на массив:

 class Aggregator

  # ...
  
  def query
    agg_data = t1.project(*columns)
                 .where(
                   t2[:id].eq('any (string_to_array(?)::int[])')
                   .and(t4[:id].eq(Arel::Nodes::BindParam.new('$2')))
                 ) 
                 .join(t2).on(t1[:id].eq(t2[:t1_id]))
                 .join(t3).on(t1[:id].eq(t3[:t1_id]))
                 .join(t4).on(t1[:id].eq(t4[:t1_id]))
                 .join(t5).on(t1[:id].eq(t5[:t1_id])) 
                 .as('agg_data')

    yield agg_data if block_given?
    t1.project('JSON_AGG(agg_data)')
      .from(agg_data)
  end

  def exec_query(t2_ids:, t4_id:)
    connection.exec_query(
      query,
      "-- CUSTOM SQL --"
      [
        [t2_ids.map {|id| Arel::Nodes.build_quoted(id) }.join(',')],
        [t4_id]
      ]
    )
  end

  # ...
end