轨。如何在使用占位符和字符串时从查询条件中删除引号?

时间:2012-01-18 23:17:52

标签: ruby-on-rails

我正在尝试创建一个搜索表单(rails 3.1),其中一个搜索参数允许用户选择数学符号​​,如&lt ;,>,= =等。然后我想使用选择的值作为我的查询。唯一的问题是它在它周围加上引号并导致无效的sql。

简化示例

params[:comparison] = '>'
params[:rank] = '3'

.where("rank ? ?", params[:comparison], params[:rank].to_i)

结果

PGError: ERROR:  syntax error at or near "3"
LINE 1: ... WHERE (rank '>' 3)

我想让它如此

WHERE (rank > 3)

如何在参数安全且不易受SQL注入攻击的情况下创建此活动记录查询而不使用大于符号的引号?

2 个答案:

答案 0 :(得分:3)

在这个非常具体的情况下,我建议您只检查params[:comparison]的值,因为您可以根据您所期望的已知安全值轻松地将其“白名单”,我认为这些值是<,>和=

示例代码:

known_comparisons = %w{< > =}
params_comparison = ">"

if known_comparisons.any? { |i| i === params_comparison }
  puts "were good"
else
  puts "bad value"
end

然后直接使用字符串插值嵌入值,因为您现在确定它是安全的。

.where("rank #{params[:comparison]} ?", params[:rank].to_i)

答案 1 :(得分:0)

这个问题让我想起了Redmine的Query类。源代码位于here

class Query < ActiveRecord::Base

@@operators = { "="   => :label_equals,
                "!"   => :label_not_equals,
                "o"   => :label_open_issues,
                "c"   => :label_closed_issues,
                "!*"  => :label_none,
                "*"   => :label_all,
                ">="  => :label_greater_or_equal,
                "<="  => :label_less_or_equal,
                "<t+" => :label_in_less_than,
                ">t+" => :label_in_more_than,
                "t+"  => :label_in,
                "t"   => :label_today,
                "w"   => :label_this_week,
                ">t-" => :label_less_than_ago,
                "<t-" => :label_more_than_ago,
                "t-"  => :label_ago,
                "~"   => :label_contains,
                "!~"  => :label_not_contains }

cattr_reader :operators

@@operators_by_filter_type = { :list => [ "=", "!" ],
                               :list_status => [ "o", "=", "!", "c", "*" ],
                               :list_optional => [ "=", "!", "!*", "*" ],
                               :list_subprojects => [ "*", "!*", "=" ],
                               :date => [ "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-" ],
                               :date_past => [ ">t-", "<t-", "t-", "t", "w" ],
                               :string => [ "=", "~", "!", "!~" ],
                               :text => [  "~", "!~" ],
                               :integer => [ "=", ">=", "<=", "!*", "*" ] }


def statement
  # filters clauses
  filters_clauses = []
  filters.each_key do |field|
    next if field == "subproject_id"
    v = values_for(field).clone
    next unless v and !v.empty?
    operator = operator_for(field)

    # "me" value subsitution
    if %w(assigned_to_id author_id watcher_id).include?(field)
      v.push(User.current.logged? ? User.current.id.to_s : "0") if v.delete("me")
    end

    sql = ''
    if field =~ /^cf_(\d+)$/
      # custom field
      db_table = CustomValue.table_name
      db_field = 'value'
      is_custom_filter = true
      sql << "#{Issue.table_name}.id IN (SELECT #{Issue.table_name}.id FROM #{Issue.table_name} LEFT OUTER JOIN #{db_table} ON #{db_table}.customized_type='Issue' AND #{db_table}.customized_id=#{Issue.table_name}.id AND #{db_table}.custom_field_id=#{$1} WHERE "
      sql << sql_for_field(field, operator, v, db_table, db_field, true) + ')'
    elsif field == 'watcher_id'
      db_table = Watcher.table_name
      db_field = 'user_id'
      sql << "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND "
      sql << sql_for_field(field, '=', v, db_table, db_field) + ')'
    elsif field == "member_of_group" # named field
      if operator == '*' # Any group
        groups = Group.all
        operator = '=' # Override the operator since we want to find by assigned_to
      elsif operator == "!*"
        groups = Group.all
        operator = '!' # Override the operator since we want to find by assigned_to
      else
        groups = Group.find_all_by_id(v)
      end
      groups ||= []

      members_of_groups = groups.inject([]) {|user_ids, group|
        if group && group.user_ids.present?
          user_ids << group.user_ids
        end
        user_ids.flatten.uniq.compact
      }.sort.collect(&:to_s)

      sql << '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'

    elsif field == "assigned_to_role" # named field
      if operator == "*" # Any Role
        roles = Role.givable
        operator = '=' # Override the operator since we want to find by assigned_to
      elsif operator == "!*" # No role
        roles = Role.givable
        operator = '!' # Override the operator since we want to find by assigned_to
      else
        roles = Role.givable.find_all_by_id(v)
      end
      roles ||= []

      members_of_roles = roles.inject([]) {|user_ids, role|
        if role && role.members
          user_ids << role.members.collect(&:user_id)
        end
        user_ids.flatten.uniq.compact
      }.sort.collect(&:to_s)

      sql << '(' + sql_for_field("assigned_to_id", operator, members_of_roles, Issue.table_name, "assigned_to_id", false) + ')'
    else
      # regular field
      db_table = Issue.table_name
      db_field = field
      sql << '(' + sql_for_field(field, operator, v, db_table, db_field) + ')'
    end
    filters_clauses << sql

  end if filters and valid?

  filters_clauses << project_statement
  filters_clauses.reject!(&:blank?)

  filters_clauses.any? ? filters_clauses.join(' AND ') : nil
end

private

# Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
  sql = ''
  case operator
  when "="
    if value.any?
      sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
    else
      # IN an empty set
      sql = "1=0"
    end
  when "!"
    if value.any?
      sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
    else
      # NOT IN an empty set
      sql = "1=1"
    end
  when "!*"
    sql = "#{db_table}.#{db_field} IS NULL"
    sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
  when "*"
    sql = "#{db_table}.#{db_field} IS NOT NULL"
    sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
  when ">="
    sql = "#{db_table}.#{db_field} >= #{value.first.to_i}"
  when "<="
    sql = "#{db_table}.#{db_field} <= #{value.first.to_i}"
  when "o"
    sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_false}" if field == "status_id"
  when "c"
    sql = "#{IssueStatus.table_name}.is_closed=#{connection.quoted_true}" if field == "status_id"
  when ">t-"
    sql = date_range_clause(db_table, db_field, - value.first.to_i, 0)
  when "<t-"
    sql = date_range_clause(db_table, db_field, nil, - value.first.to_i)
  when "t-"
    sql = date_range_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
  when ">t+"
    sql = date_range_clause(db_table, db_field, value.first.to_i, nil)
  when "<t+"
    sql = date_range_clause(db_table, db_field, 0, value.first.to_i)
  when "t+"
    sql = date_range_clause(db_table, db_field, value.first.to_i, value.first.to_i)
  when "t"
    sql = date_range_clause(db_table, db_field, 0, 0)
  when "w"
    first_day_of_week = l(:general_first_day_of_week).to_i
    day_of_week = Date.today.cwday
    days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
    sql = date_range_clause(db_table, db_field, - days_ago, - days_ago + 6)
  when "~"
    sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
  when "!~"
    sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
  end

  return sql
end

...

end