Search code examples
sqlruby-on-railsrubysql-injectionbrakeman

Rails brakeman warning of sql injection


I've got a scope in my model :

scope :assigned_to_user, ->(user) {
task_table = UserTask.table_name

    joins("INNER JOIN #{task_table}
          ON  #{task_table}.user_id = #{user.id}
          AND (#{task_table}.type_id = #{table_name}.type_id)
          AND (#{task_table}.manager_id = #{table_name}.manager_id)
        ")
}

So after running brakeman report I get this warning :

assigned_to_user | SQL Injection | Possible

So I tried the following :

scope :assigned_to_user, ->(user) {
    task_table = UserTask.table_name

        joins(ActiveRecord::Base::sanitize("INNER JOIN #{task_table}
              ON  #{task_table}.user_id = #{user.id}
              AND (#{task_table}.type_id = #{table_name}.type_id)
              AND (#{task_table}.manager_id = #{table_name}.manager_id)
            "))
    }

This doesn't work for me because it adds ' (apostrophe) to the front and back of the sql. So when I use this as a part of query which returns some results and I apply this scope it generates the incorrect sql.

I also tried this:

scope :assigned_to_user, ->(user) {
    task_table = UserTask.table_name

        joins("INNER JOIN #{task_table}
              ON  #{task_table}.user_id = ?
              AND (#{task_table}.type_id = #{table_name}.type_id)
              AND (#{task_table}.manager_id = #{table_name}.manager_id)
            ", user.id)
    }

Doesn't even build the statement. And tried couple of other stuff which didn't work and not even worth mentioning. Does anybody have idea how to fix this?


Solution

  • After some kind of research here is what I would use. There is a method called sanitize_sql_array (ref), you can use it to escape statements by passing an sql string and replacement values to it like:

    sanitize_sql_array(['user_id = :user_id', user_id: 5])
    # => "user_id = 5"
    

    If we'd pass a table name to this method it will also escape it, but will apply a quote method of ActiveRecord::Base.connection object on a value, which is used to escape variables, but not table names. Maybe sometimes it will work, but it failed for me when I was using PostrgreSQL, because quote method uses single quotes, but PostgreSQL requires double-quotation for table names.

    sanitize_sql_array([
      'INNER JOIN :table_name ON :table_name.user_id = :user_id',
      { table_name: 'users', user_id: 5 }
    ])
    # => "INNER JOIN 'users' ON 'users'.user_id = 5"
    

    connection object also has a method quote_table_name, which could be separately applied on table names, to make sure that they are escaped + use sanitize_sql_array for user id.

    scope :assigned_to_user, -> (user) {
      task_table = connection.quote_table_name(UserTask.table_name)
      current_table = connection.quote_table_name(table_name)
      sanitized_sql = sanitize_sql_array([
        "INNER JOIN #{task_table}
        ON  #{task_table}.user_id = :user_id
        AND (#{task_table}.type_id = #{current_table}.type_id)
        AND (#{task_table}.manager_id = #{current_table}.manager_id)",
        { user_id: user.id }
      ])
      joins(sanitized_sql)
    }
    

    Or you could actually just use sanitize on user.id instead of wrapping everything in sanitize_sql_array method call (#{sanitize(user.id)}).

    By the way, Brakeman won't already show any warnings, because query has been moved to a variable. Brakeman literally parses your code as is and it does not know about a variable and it's content. So all this thing is just to make yourself sure that everything is being escaped.

    Just to shut up Brakeman you could just move a query to a variable:

    scope :assigned_to_user, -> (user) {
      task_table = UserTask.table_name
      query = "INNER JOIN #{task_table}
              ON  #{task_table}.user_id = #{user.id}
              AND (#{task_table}.type_id = #{table_name}.type_id)
              AND (#{task_table}.manager_id = #{table_name}.manager_id)"
      joins(query)
    }