Rails, how to sanitize SQL in find_by_sql - ruby-on-rails

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).

Related

how to match all multiple conditions with rails where method

Using active record, I want to perform a lookup that returns a collection of items that have ALL matching id's.
Given that the below example matches on ANY id in the array, I am trying to figure out the syntax so that it will match when ALL of the id's match. (given that in this example there is a many to many relationship).
The array length of the id's is also variable which prohibits chaining .where()
x.where(id: [1,2])
Note: this question got removed before and there are a lot of answers for performing a sql "where in" but this question is about performing a sql "where and"
You can use exec_query and execute your own bound query:
values = [1, 2]
where_condition = values.map.with_index(1) { |_, index| "id = $#{index}" }.join(" AND ")
sql = "SELECT * FROM table WHERE #{ where_condition }"
binds = values.map { |i| ActiveRecord::Relation::QueryAttribute.new(nil, i, ActiveRecord::Type::Integer.new) }
ActiveRecord::Base.connection.exec_query(sql, nil, binds)
I completely agree with #muistooshort's comment
where(id: [1,2]) doesn't make sense unless you're joining to an association table and in that case,..."where in" combined with HAVING [solves your problem].
But for the sake of answering the question and the assumption that id was just and example.
While #SebastianPalma's answer will work it will return an ActiveRecord::Result whereas most of the time the desire is an ActiveRecord::Relation.
We can achieve this by using Arel to build the where clause like so:
(I modified the example to use description rather than id so that it makes more logical sense)
table = MyObject.arel_table
values = ['Jamesla','Example']
where_clause = values.map {|v| table[:description].matches("%{v}%")}.reduce(&:and)
# OR
where_clause = table[:description].matches_all(values.map {|v| "%#{v}%"})
MyObject.where(where_clause)
This will result in the following SQL query:
SELECT
my_objects.*
FROM
my_objects
WHERE
my_objects.description LIKE '%Jamesla%'
AND my_objects.description LIKE '%Example%'

rails : how can I use find_each with map?

I have a form that has nested field (habtm and accepts_nested_attributes_for). That form contains with a field "keywords", that autocompletes keywords that come from a postgresql table.
All that works well. This is in params :
"acte"=>{"biblio_id"=>"1", "keywords"=>{"keywords"=>"judge, ordeal, "}
What I now need to do is take those keywords and get their keywords_id out of the table keywords. Those id must be added to the join table.
I'm doing this :
q = params[:acte][:keywords].fetch(:keywords).split(",")
a = q.map {|e| Keyword.find_by keyword: e }
As per the guides, find_by returns only the first matching field. I guess I would need to use find_each but I'm not certain about that and I can't get it to word. I have tried this:
q = params[:acte][:motclefs].fetch(:motclefs).split(",")
a = Array.new
Motclef.where(motcle: q).find_each do |mot|
a << mot.id
end
This also finds only the first result like : [251].
What I'm looking to get is something like [1453, 252, 654]
thanks !
Putting find_by in a loop means you will be executing a separate SQL query for each SQL keyword.
You can instead just get all the ids in a single SQL call by doing keyword in.
After you do q = params[:acte][:keywords].fetch(:keywords).split(","), your q will be an array of keywords. So q will be ["judge", " ordeal"].
You can simply do Keyword.where(keyword: q).select(:id) which will generate a query like SELECT keywords.id FROM keywords where keyword in ('judge', 'ordeal').

ruby map find_each can't add to array

I have 2 Models: Document and Keywords. They are habtm in relation to each other and they both accepts_nested_attributes_for each other so I can create a nested form. That works well.
So in params, I have
"document"=>{"book_id"=>"1", "keywords"=>{"keywords"=>"term, administration, witness "}, ...
In the controller I put the keywords in a separate array like this :
q = params[:document][:keywords].fetch(:keywords).split(",")
This works well too.
What I now need to do, is get the keywords ids and put them in an array. Each element of that array will populate the join table.
I've tried this :
a = Array.new
q.each do |var|
id = Keyword.select(:id).find_by keyword: var
a << id
id
end
But, this only answers [#<Keyword id: 496>, nil, nil], although the server log shows that all 3 SQL requests are executed and they are correct.
I have also tried this :
a = Array.new
q.map do |e|
Keyword.where(motcle: e).select(:id).find_each do |wrd|
a << wrd.id
end
end
Then again, this only return the FIRST id of the keyword, although the server log shows that all 3 SQL requests are executed.
What I'm trying to get is a = [496, 367, 2398]
So I have 2 questions :
1/ Why are the ids not added to the array, despite the server executing all SQL requests ?
2/ How to write in rails a request would be
SELECT "motclefs"."id" FROM "motclefs" WHERE "motclefs"."motcle" in ('déchéances','comtesse') ORDER BY "motclefs"."id";
Thanks !
The returned value is an object of Keyword. You need to get the id attribute of the object. eg:
id = Keyword.where(keyword: var).select(:id).first.id
A better way to get all the ids would be
a = Keyword.where(keyword: ['term', 'administration', 'witness']).pluck(:id)
# I think this might answer your second question.

Avoid sql injection with connection.execute

If a query can't be efficiently expressed using ActiveRecord, how to safely use ActiveRecord::Base.connection.execute when interpolating passed params attributes?
connection.execute "... #{params[:search]} ..."
You can use the methods in ActiveRecord::Sanitization::ClassMethods.
You do have to be slightly careful as they are protected and therefore only readily available for ActiveRecord::Base subclasses.
Within a model class you could do something like:
class MyModel < ActiveRecord::Base
def bespoke_query(params)
query = sanitize_sql(['select * from somewhere where a = ?', params[:search]])
connection.execute(query)
end
end
You can send the method to try it out on the console too:
> MyModel.send(:sanitize_sql, ["Evening Officer ?", "'Dibble'"])
=> "Evening Officer '\\'Dibble\\''"
ActiveRecord has a sanitize method that allows you to clean the query first.
Perhaps it's something you can look into: http://apidock.com/rails/v4.1.8/ActiveRecord/Sanitization/ClassMethods/sanitize
I'd be very careful inserting parameters directly like that though.
What problem are you experiencing, that you cannot use ActiveRecord?
You can use functions from ActiveRecord::Base to sanitize your sql query. E.g. sanitize_sql_array. As mentioned in other answers they are protected, but that's possible to get around without having to deal with inheritance.
sanitize_sql_array accepts an array of strings where the first element is the query and the subsequent elements will replace ? characters in the query.
query = 'SELECT * FROM users WHERE id = ? OR first_name = ?'
id = 1
name = 'Alice'
sanitized_query = ActiveRecord::Base.send(:sanitize_sql_array, [query, id, name])
response = ActiveRecord::Base.connection.execute(sanitized_query)

Ordering by specific value in Activerecord

In Ruby on Rails, I'm trying to order the matches of a player by whether the current user is the winner.
The sort order would be:
Sort by whether the current user is the winner
Then sort by created_at, etc.
I can't figure out how to do the equivalent of :
Match.all.order('winner_id == ?', #current_user.id)
I know this line is not syntactically correct but hopefully it expresses that the order must be:
1) The matches where the current user is the winner
2) the other matches
You can use a CASE expression in an SQL ORDER BY clause. However, AR doesn't believe in using placeholders in an ORDER BY so you have to do nasty things like this:
by_owner = Match.send(:sanitize_sql_array, [ 'case when winner_id = %d then 0 else 1 end', #current_user.id ])
Match.order(by_owner).order(:created_at)
That should work the same in any SQL database (assuming that your #current_user.id is an integer of course).
You can make it less unpleasant by using a class method as a scope:
class Match < ActiveRecord::Base
def self.this_person_first(id)
by_owner = sanitize_sql_array([ 'case when winner_id = %d then 0 else 1 end', id])
order(by_owner)
end
end
# and later...
Match.this_person_first(#current_user.id).order(:created_at)
to hide the nastiness.
This can be achived using Arel without writing any raw SQL!
matches = Match.arel_table
Match
.order(matches[:winner_id].eq(#current_user.id).desc)
.order(created_at: :desc)
Works for me with Postgres 12 / Rails 6.0.3 without any security warning
If you want to do sorting on the ruby side of things (instead of the SQL side), then you can use the Array#sort_by method:
query.sort_by(|a| a.winner_id == #current_user.id)
If you're dealing with bigger queries, then you should probably stick to the SQL side of things.
I would build a query and then execute it after it's built (mostly because you may not have #current_user. So, something like this:
query = Match.scoped
query = query.order("winner_id == ?", #current_user.id) if #current_user.present?
query = query.order("created_at")
#results = query.all

Resources