使用Postgres强制转换将动态列名安全地传递给ActiveRecord查询?

时间:2016-09-27 21:05:12

标签: ruby-on-rails postgresql activerecord rails-activerecord

我在我的应用程序中没有日期查询时花了很多时间,我想抽出一些问题。

所以说我有一个DateTime starts_at字段的模型:

Shift.where('starts_at::time > ?', '20:31:00.00')
-> SELECT "shifts".* FROM "shifts" WHERE (starts_at::time > '20:31:00.00')

这正确地返回了所有' starts_at'值大于时间20:31。

我想动态地将列名传递给查询,所以我可以这样做:

Shift.where('? > ?', "#{column_name}::time", '20:31:00.00').
-> SELECT "shifts".* FROM "shifts" WHERE ('starts_at::time' > '20:31:00.00')

在此示例中,这不起作用,因为搜索将starts_at::time作为字符串执行,而不是作为time强制转换的列。

如何安全地将column_name传递到::time投射的查询中?虽然这不会接受用户输入,但我仍然希望确保考虑SQL注入。

2 个答案:

答案 0 :(得分:14)

这比你最初想象的要复杂得多,因为标识符(列名,表名,......)和值('pancakes'6,...)是非常不同的东西具有不同引用规则甚至引用字符的SQL(字符串的单引号,标准SQL中标识符的双引号,MySQL中标识符的反引号,SQL-Server中标识符的括号,......)。如果您考虑像Ruby变量名称这样的标识符,以及类似于文字Ruby值的标识符,那么您可以开始看到差异。

当你这样说时:

where('? > ?', ...)

两个占位符都将被视为值(不是标识符)并引用。为什么是这样? ActiveRecord无法知道哪个?应该是标识符(例如created_at列名称),哪个应该是值(例如20:31:00.00)。

数据库连接确实有一个专门用于引用列名的方法:

> puts ActiveRecord::Base.connection.quote_column_name('pancakes')
"pancakes"
=> nil

所以你可以这样说:

quoted_column = Shift.connection.quote_column_name(column_name)
Shift.where("#{quoted_name}::time > ?", '20:31:00.00')

这有点令人不愉快,因为我们在使用字符串插值来构建SQL时会退缩(或至少我们应该)。但是,quote_column_name会处理column_name中任何狡猾或不安全的事情,因此这实际上并不危险。

你也可以说:

quoted_column = "#{Shift.connection.quote_column_name(column_name)}::time"
Shift.where("#{quoted_name} > ?", '20:31:00.00')

如果您不总是需要将列名转换为time。甚至:

clause = "#{Shift.connection.quote_column_name(column_name)}::time > ?"
Shift.where(clause, '20:31:00.00')

您也可以使用extract or one of the other date/time functions代替类型转换,但仍然会留下引用问题以及有点畏缩的quote_column_name电话。

另一种选择是将column_name列入白名单,以便只允许特定的有效值。然后你可以将安全column_name权限放入查询中:

if(!in_the_whitelist(column_name))
  # Throw a tantrum, hissy fit, or complain in your preferred fashion
end
Shift.where("#{column_name} > ?", '20:31:00.00')

只要您没有像"gotta have some breakfast"这样的任何时髦的列名或者总是需要正确引用的类似内容,这应该没问题。您甚至可以使用Shift.column_namesShift.columns来构建白名单。

同时使用白名单和quote_column_name可能是最安全的,但quote_column_name方法应该足够了。

答案 1 :(得分:0)

我决定利用Rails列的命名约定来使用这种小型解决方案:

  scope :field_before_and_on_date, -> (field, time) do
    column_name = field.to_s.parameterize.underscore
    where("#{column_name} <= ?", time.end_of_day)
  end

# Takes advantage of:
> "); and delete everything(); stuff(".parameterize.underscore
=> "and_delete_everything_stuff"

它是有限的,但该概念也适用于类型转换。