如何将 exec_query 与动态 SQL 一起使用

Posted

技术标签:

【中文标题】如何将 exec_query 与动态 SQL 一起使用【英文标题】:How to use exec_query with dynamic SQL 【发布时间】:2021-10-22 08:24:15 【问题描述】:

我正在处理一个查询,并使用 exec_query 进行绑定以避免潜在的 SQL 注入。但是,我在尝试检查 id 是否在数组中时遇到了问题。

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 是错误的解决方法,我肯定会尝试其他事情:D

在我的课堂上,我使用 AR 的内部 sql 注入预防来搜索第一个绑定变量 id,然后提取 id 并加入到 sql 查询的字符串中。我对另一个绑定变量做同样的事情,找到对象并使用那个 id。只是作为进一步的预防措施。因此,当用户输入用于查询时,他们已经通过了 AR。这是一个刹车员扫描,它抛出了错误。我在星期一与我们的安全团队开会讨论这个问题,但也想在这里查看:D

【问题讨论】:

你能告诉我们实际的查询吗?仅凭图片的一小部分就很难真正提供一个像样的答案?是不是可以用 AR 查询界面和 Arel 而不是字符串来组合? 我认为 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);

Rails 已为您服务!

【讨论】:

我知道内置 SQL 注入预防的 AR。我会更新这个问题,但我有一个我认为对于 AR 来说有点过于复杂的选择。另外,我认为由于我不需要数据库中的实际 AR 对象,我觉得原始 sql 性能更好。 感谢您的建议。我正在使用 AR 来清理输入:P 我正在获取 id 集合,找到 AR 对象集合,然后将 id 重新加入到原始查询的字符串中。我希望找到一种我不必这样做的方法。但是我在工作中与我的安全人员会面,他们告诉我,由于我的预处理,这是刹车员的误报。感谢您加强我的选择:D【参考方案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 ?

这意味着您在准备查询时必须事先知道绑定变量的数量。作为一个 hacky 解决方法,您可以使用一些创造性的类型转换来让 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

【讨论】:

以上是关于如何将 exec_query 与动态 SQL 一起使用的主要内容,如果未能解决你的问题,请参考以下文章

如何将 Spring AbstractRoutingDataSource 与动态数据源一起使用?

如何在 Sql Server Compact Edition 中将参数与 LIKE 一起使用

我们可以将谷歌云 SQL 与 Amazon Elastic Beanstalk 一起使用吗

如何将变量值与 select 语句的结果一起放入 sql 表中?

如何将 @FetchRequest 属性包装器的新 nsPredicate 动态属性与传递给 View 的对象一起使用

如何将 SQL 比较条件 ANY/SOME 与 DATATYPE Char 一起使用