我在我的应用程序中没有日期查询时花了很多时间,我想抽出一些问题。
所以说我有一个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注入。
答案 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_names
或Shift.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"
它是有限的,但该概念也适用于类型转换。