Dynamic where clause in active record avoiding sql injection - ruby-on-rails

do you know how to build a dynamic query avoiding sql injection
?
property = 'foo'
value = 'bar'
SomeObject.where("#{property} > ?", value)
# works but permit sql inj
SomeObject.where(":property > :value", property: property, value: value)
# create select * from some_object where 'foo' > 'bar'
# and the 'foo' I need without the quotes

SomeObject.where(
"#{SomeObject.connection.quote_column_name(property)} > :value",
value: value
)
UPDATE
Example 1 (trying to end the statement and inject a new one):
property = '; DROP TABLE users; --'
User.where("#{User.connection.quote_column_name(property)} > :value", value: 3)
# => SELECT "users".* FROM "users" WHERE ("; DROP TABLE users; --" > 3)
Example 2 (trying to end the column name quote):
property = '"; DELETE FROM users;--'
User.where("#{User.connection.quote_column_name(property)} > :value", value: 3)
# => SELECT "users".* FROM "users" WHERE ("""; DELETE FROM users;--" > 3)

Not sure of your use case but arel can help you with this like so
some_object_table = SomeObject.arel_table
SomeObject.where(some_object_table[property.intern].gt(value))
This will execute the query appropriately with all the escaping you have come to love with rails.
This works because arel is the underlying query assembler used by rails so ActiveRecord where clauses can understand Arel::Nodes without issue (its actually how they are assembled to begin with)
Also given the dynamic nature you may want to check that property is a valid column to avoid SQL level errors something like
raise AgrumentError unless some_object_table.engine.columns.map {|c| c.name.intern}.include?(property.intern)
# or
raise AgrumentError unless SomeObject.column_names.map(&:to_sym).include?(property.to_sym)

A simple but secure way would be to whitelist the allowed property names:
PROPERTIES = ["foo", "bar", "baz"].freeze
def find_greater_than(property, value)
raise "'#{property}' is not a valid property, only #{PROPERTIES.join(", ")} are allowed!" if !PROPERTIES.include?(property)
SomeObject.where("#{property} > ?", value)
end
You can (as #engineersmnky pointed out) dynamically check for available columns:
raise "Some Message" if SomeObject.column_names.include?(property)
but I don't like this aproach as having columns searchable should be a decission, not automated.
Another aproach is to use the sanitizing provided by Rails.
def find_greater_than(property, value)
sanitized_property = ActiveRecord::Base.connection.quote_column_name(property)
SomeObject.where("#{sanitized_property} > ?", value)
end
The quoting logic is implemented by the DB specific connection adapters.

In my case, I ended by using
ActiveRecord::Base.connection.quote_table_name function to sanitize the column name before queries.
property = 'foo'
value = 'bar'
sanitized_property = ActiveRecord::Base.connection.quote_table_name(property)
SomeObject.where("#{sanitized_property} > ?", value)
that function can handle table and column name definition.
property = 'table.column'
will produce
where `table`.`column` > `bar`

Related

Why does .order doesn't get executed in my query?

I have slightly modified version for Kaminari to find on which page my record is (original code is here: https://github.com/kaminari/kaminari/wiki/FAQ#how-can-i-know-which-page-a-record-is-on):
module KaminariHelper
extend ActiveSupport::Concern
# Detecting page number in pagination. This method allows you to specify column name, order,
# items per page and collection ids to filter specific collection.
#
# Options:
# * :by - specifies the column name. Defaults to :id.
# * :order - specifies the order in DB. Defaults to :asc.
# * :per - specifies amount of items per page. Defaults to object model's default_per_page.
# * :nulls_last - if set to true, will add "nulls last" into the order.
# * :collection_ids - array of ids for the collection of current class to search position in specific collection.
# If it's ommited then search is done across all objects of current object's model.
#
def page_num(options = {})
column = options[:by] || :id
order = options[:order] || :asc
per = options[:per] || self.class.default_per_page
nulls_last = options[:nulls_last] == true ? "nulls last" : ""
operator = (order == :asc ? "<=" : ">=")
data = if options[:collection_ids].present?
self.class.where(id: options[:collection_ids])
else
self.class
end
# 1. Get a count number of all the results that are listed before the given record/value.
# 2. Divide the count by default_per_page or per number given as an option.
# 3. Ceil the number to get a proper page number that the record/value is on.
(
data.where("#{column} #{operator} ?", read_attribute(column))
.order("#{column} #{order} #{nulls_last}").count.to_f / per
).ceil
end
end
However when I test it for some weird reasons .order doesn't seem to be executed. Here is the sql output in rails console:
2.3.1 :005 > email.page_num(by: :sent_at, order: :desc, per: 25, collection_ids: user.emails.ids, nulls_last: true)
(1.1ms) SELECT "emails"."id" FROM "emails" WHERE "emails"."deleted_at" IS NULL
AND "emails"."user_id" = $1 [["user_id", 648]]
(1.5ms) SELECT COUNT(*) FROM "emails" WHERE "emails"."deleted_at" IS NULL AND
"emails"."id" IN (35946, 41741) AND (sent_at >= '2016-01-22 14:04:26.700352')
=> 13
Why does .order is not applied in the final SQL query? Is there a way to execute it? Otherwise the code doesn't make sense since there are no guarantee it'll give me proper page number.
Rails does ignore the order clause when counting.
Instead of relying on Rails' count method, try counting manually:
data.where("#{column} #{operator} ?", read_attribute(column))
.order("#{column} #{order} #{nulls_last}")
.select('COUNT(*) c').first.c.to_f / per

Rails ActiveRecord 4: correct way to write a greater than

I know that
Object.where('key > ?', value)
works.
But if the query happens to have several tables involved, with multiple key columns, it might break as the query produced is:
SELECT "tablename".* FROM "tablename" WHERE "tablename"."user_id" = $1 AND (key > 0) [["user_id", 29]]
A solution would be
Object.where('tablename.key > ?', value)
But ain't there an arel way to write this instead? My app has (enforced) weird table names, I'd rather not write them there and that they get added dynamically by active record.
Thanks
I'd personally still try to stay with AR on that one, and do something with a range and a hash query:
Object.where(tablename: { key: value..Float::INFINITY}) # If value is a number
Object.where(tablename: { key: value..DateTime::Infinity.new}) # If value is a DateTime
It's a bit verbose, but you can use arel to do this. For example
Object.where(Object.arel_table[:key].gt(123))
will select objects where key > 123.
If I was doing this, I would probably define some helper methods, perhaps something along the lines of
class Foo < ActiveRecord::Base
def self.column(name)
Foo.arel_table[name]
end
#now you can do
def self.some_method
Foo.where(column(:key).gt(123))
end
end
If you're querying from one object (one table) you can drop the table name in the where clause.
Object.where('key > ?', value)
Unfortunately that's the best way there is to do it.

Rails methods vulnerable to SQL injection?

What are the Rails methods that are vulnerable to SQL injection, and in what form?
For example, I know that where with a string argument is vulnerable:
Model.where("name = #{params[:name]}") # unsafe
But a parameterized string or hash is not:
Model.where("name = ?", params[:name]) # safe
Model.where(name: params[:name]) # safe
I'm mostly wondering about where, order, limit and joins, but would like to know about any other methods that might be attack vectors.
In Rails, where, order, limit and joins all have vulnerable forms. However, Rails limits the number of SQL operations performed to 1 so vulnerability is limited. An attacker cannot end a statement and execute a new arbitrary one.
Where
Where has one vulnerable form: string.
# string, unsafe
Model.where("name = '#{params[:name]}'")
# hash/parameterized string/array, safe
Model.where(name: params[:name])
Model.where("name = ?", params[:name])
Model.where(["name = ?", params[:name]])
Order
String form is vulnerable:
# unsafe
params[:order] = "1; --\n drop table users;\n --"
Model.order("#{params[:order]} ASC")
# safe
order_clause = sanitize(params[:order])
Model.order(order_clause)
Limit
Limit has no vulnerable forms, since Rails casts input to Integer beforehand.
Model.limit("1; -- \n SELECT password from users; -- ")
=> ArgumentError: invalid value for Integer(): "1; -- \n SELECT password from users; -- "
Joins
String form is vulnerable:
params[:table] = "WHERE false <> $1; --"
Model.where(:user_id => 1).joins(params[:table])
=> SELECT "models".* FROM "models" WHERE false <> $1 -- WHERE "models"."user_id" = $1 [["user_id", 1]]
Much more comprehensive information can be found at rails-sqli.org.
Generally: If you let the user input and save any text into your database, without escaping code, it could harm your system. Especially if these texts may contain tags/code snippets.

Are the .order method parameters in ActiveRecord sanitized by default?

I'm trying to pass a string into the .order method, such as
Item.order(orderBy)
I was wondering if orderBy gets sanitized by default and if not, what would be the best way to sanitize it.
The order does not get sanitized. This query will actually drop the Users table:
Post.order("title; drop table users;")
You'll want to check the orderBy variable before running the query if there's any way orderBy could be tainted from user input. Something like this could work:
items = Item.scoped
if Item.column_names.include?(orderBy)
items = items.order(orderBy)
end
They are not sanitized in the same way as a .where clause with ?, but you can use #sanitize_sql_for_order:
sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
# => "field(id, 1,3,2)"
sanitize_sql_for_order("id ASC")
# => "id ASC"
http://api.rubyonrails.org/classes/ActiveRecord/Sanitization/ClassMethods.html#method-i-sanitize_sql_for_order
Just to update this for Rails 5+, as of this writing, passing an array into order will (attempt to) sanitize the right side inputs:
Item.order(['?', "'; DROP TABLE items;--"])
#=> SELECT * FROM items ORDER BY '''; DROP TABLE items;--'
This will trigger a deprecation warning in Rails 5.1 about a "Dangerous query method" that will be disallowed in Rails 6. If you know the left hand input is safe, wrapping it in an Arel.sql call will silence the warning and, presumably, still be valid in Rails 6.
Item.order([Arel.sql('?'), "'; DROP TABLE items;--"])
#=> SELECT * FROM items ORDER BY '''; DROP TABLE items;--'
It's important to note that unsafe SQL on the left side will be sent to the database unmodified. Exercise caution!
If you know your input is going to be an attribute of your model, you can pass the arguments as a hash:
Item.order(column_name => sort_direction)
In this form, ActiveRecord will complain if the column name is not valid for the model or if the sort direction is not valid.
I use something like the following:
#scoped = #scoped.order Entity.send(:sanitize_sql, "#{#c} #{#d}")
Where Entity is the model class.
Extend ActiveRecord::Relation with sanitized_order.
Taking Dylan's lead I decided to extend ActiveRecord::Relation in order to add a chainable method that will automatically sanitize the order params that are passed to it.
Here's how you call it:
Item.sanitized_order( params[:order_by], params[:order_direction] )
And here's how you extend ActiveRecord::Relation to add it:
config/initializers/sanitized_order.rb
class ActiveRecord::Relation
# This will sanitize the column and direction of the order.
# Should always be used when taking these params from GET.
#
def sanitized_order( column, direction = nil )
direction ||= "ASC"
raise "Column value of #{column} not permitted." unless self.klass.column_names.include?( column.to_s )
raise "Direction value of #{direction} not permitted." unless [ "ASC", "DESC" ].include?( direction.upcase )
self.order( "#{column} #{direction}" )
end
end
It does two main things:
It ensures that the column parameter is the name of a column name of the base klass of the ActiveRecord::Relation.
In our above example, it would ensure params[:order_by] is one of Item's columns.
It ensures that the direction value is either "ASC" or "DESC".
It can probably be taken further but I find the ease of use and DRYness very useful in practice when accepting sorting params from users.

Rails, how to sanitize SQL in find_by_sql

Is there a way to sanitize sql in rails method find_by_sql?
I've tried this solution:
Ruby on Rails: How to sanitize a string for SQL when not using find?
But it fails at
Model.execute_sql("Update users set active = 0 where id = 2")
It throws an error, but sql code is executed and the user with ID 2 now has a disabled account.
Simple find_by_sql also does not work:
Model.find_by_sql("UPDATE user set active = 0 where id = 1")
# => code executed, user with id 1 have now ban
Edit:
Well my client requested to make that function (select by sql) in admin panel to make some complex query(joins, special conditions etc). So I really want to find_by_sql that.
Second Edit:
I want to achieve that 'evil' SQL code won't be executed.
In admin panel you can type query -> Update users set admin = true where id = 232 and I want to block any UPDATE / DROP / ALTER SQL command.
Just want to know, that here you can ONLY execute SELECT.
After some attempts I conclude sanitize_sql_array unfortunatelly don't do that.
Is there a way to do that in Rails??
Sorry for the confusion..
Try this:
connect = ActiveRecord::Base.connection();
connect.execute(ActiveRecord::Base.send(:sanitize_sql_array, "your string"))
You can save it in variable and use for your purposes.
I made a little snippet for this that you can put in initializers.
class ActiveRecord::Base
def self.escape_sql(array)
self.send(:sanitize_sql_array, array)
end
end
Right now you can escape your query with this:
query = User.escape_sql(["Update users set active = ? where id = ?", true, params[:id]])
And you can call the query any way you like:
users = User.find_by_sql(query)
Slightly more general-purpose:
class ActiveRecord::Base
def self.escape_sql(clause, *rest)
self.send(:sanitize_sql_array, rest.empty? ? clause : ([clause] + rest))
end
end
This one lets you call it just like you'd type in a where clause, without extra brackets, and using either array-style ? or hash-style interpolations.
User.find_by_sql(["SELECT * FROM users WHERE (name = ?)", params])
Source: http://blog.endpoint.com/2012/10/dont-sleep-on-rails-3-sql-injection.html
Though this example is for INSERT query, one can use similar approach for UPDATE queries. Raw SQL bulk insert:
users_places = []
users_values = []
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
params[:users].each do |user|
users_places << "(?,?,?,?)" # Append to array
users_values << user[:name] << user[:punch_line] << timestamp << timestamp
end
bulk_insert_users_sql_arr = ["INSERT INTO users (name, punch_line, created_at, updated_at) VALUES #{users_places.join(", ")}"] + users_values
begin
sql = ActiveRecord::Base.send(:sanitize_sql_array, bulk_insert_users_sql_arr)
ActiveRecord::Base.connection.execute(sql)
rescue
"something went wrong with the bulk insert sql query"
end
Here is the reference to sanitize_sql_array method in ActiveRecord::Base, it generates the proper query string by escaping the single quotes in the strings. For example the punch_line "Don't let them get you down" will become "Don\'t let them get you down".
I prefer to do it with key parameters. In your case it may looks like this:
Model.find_by_sql(["UPDATE user set active = :active where id = :id", active: 0, id: 1])
Pay attention, that you pass ONLY ONE parameter to :find_by_sql method - its an array, which contains two elements: string query and hash with params (since its our favourite Ruby, you can omit the curly brackets).

Resources