rails dynamic where sql query - ruby-on-rails

I have an object with a bunch of attributes that represent searchable model attributes, and I would like to dynamically create an sql query using only the attributes that are set. I created the method below, but I believe it is susceptible to sql injection attacks. I did some research and read over the rails active record query interface guide, but it seems like the where condition always needs a statically defined string as the first parameter. I also tried to find a way to sanitize the sql string produced by my method, but it doesn't seem like there is a good way to do that either.
How can I do this better? Should I use a where condition or just somehow sanitize this sql string? Thanks.
def query_string
to_return = ""
self.instance_values.symbolize_keys.each do |attr_name, attr_value|
if defined?(attr_value) and !attr_value.blank?
to_return << "#{attr_name} LIKE '%#{attr_value}%' and "
end
end
to_return.chomp(" and ")
end

Your approach is a little off as you're trying to solve the wrong problem. You're trying to build a string to hand to ActiveRecord so that it can build a query when you should simply be trying to build a query.
When you say something like:
Model.where('a and b')
that's the same as saying:
Model.where('a').where('b')
and you can say:
Model.where('c like ?', pattern)
instead of:
Model.where("c like '#{pattern}'")
Combining those two ideas with your self.instance_values you could get something like:
def query
self.instance_values.select { |_, v| v.present? }.inject(YourModel) do |q, (name, value)|
q.where("#{name} like ?", "%#{value}%")
end
end
or even:
def query
empties = ->(_, v) { v.blank? }
add_to_query = ->(q, (n, v)) { q.where("#{n} like ?", "%#{v}%") }
instance_values.reject(&empties)
.inject(YourModel, &add_to_query)
end
Those assume that you've properly whitelisted all your instance variables. If you haven't then you should.

Related

Using column_names.include? to protect against SQL Injection?

I have a rails API that handles requests from my front end. These requests include query parameters in the url for refining and sorting results from the database. An example URL query looks like this:
http://localhost:8000/clients?_sort=name&_order=DESC&_start=0&_end=10
My index method in my controller grabs these params and uses them for filtering and sorting:
def index
#all_clients = Client.all
response.headers['X-Total-Count'] = #all_clients.count
if (Client.column_names.include?(params[:_sort]))
if (params[:_order] == 'ASC')
#clients = Client.filtered(params[:_start].to_i, params[:_end].to_i).order("#{params[:_sort]} asc")
else
#clients = Client.filtered(params[:_start].to_i, params[:_end].to_i).order("#{params[:_sort]} desc")
end
end
json_response(#clients || #all_clients)
end
the filtered method is a scope which looks like this:
scope :filtered, -> (_start, _end) { limit(_end-_start).offset(_start) }
My question is this: by using Client.column_names.include? to check if params[:_sort] is a valid attribute to sort by, am I effectively whitelisting against SQL Injection? If not, how could I alter this code to protect against SQL Injection?
The important thing to consider here is not "the whitelisting of params" (since you're already cherry-picking which parameters to use anyway, rather than blindly using the whole params hash for something), but rather how you are constructing the SQL.
There are two potential injection areas in the code:
limit(_end-_start)
Is this vulnerable? No. If _end or _start are anything besides integers, then the code will just fail with an error message - such as:
NoMethodError: undefined method `-' for "DROP_TABLE":String
or
ArgumentError: invalid value for Integer(): 3.14159
order("#{params[:_sort]} desc")
Is this vulnerable? Yes. (But not easily.) This page gives a concrete example:
params[:_sort] = "(CASE SUBSTR(password, 1, 1) WHEN 's' THEN 0 else 1 END)"
You should never use direct string interpolation in SQL, unless you are absolutely 100% sure that the string is "safe". In this case, you could just write it as:
order(params[:_sort] => :asc)

How to Dynamically Build an Active Record Query with Rails Scopes

I am looking for a better way to dynamically build a active record query without making a sql string.
The following method does a new search for every word in the search_str and returns the records that are returned by all the search scopes.
scope :multi_search, ->(search_str){
query = ''
if search_str.present?
search_str.split(' ').each do |x|
query += ".search('#{x}')"
end
eval(query[1..-1])
else
all
end
}
It works, but this is not a clean implementation with the use of eval. Is there a better way of doing this?
In a model method:
def self.multi_search(your_params)
scope = Model.scoped({})
your_params.split(' ').map{|v| scope = scope.search(v)}
scope
end

Rails dynamic attribute in where LIKE clause

I have a search method, which takes in a key value pair in argument and searches on an active record model via a LIKE query. But I am unable to get it to work. It doesn't take the key argument properly.
This is what my method looks like:
def search(key,value)
where('? LIKE ?',key,"%#{value}%")
end
The query it fires is ('name' LIKE '%air%') whereas it should fire (name LIKE '%air%')
Is there a way I could get this to work?
Warning: The solution proposed by #MKumar is very dangerous. If key is user-input, you just allowed SQL injection.
def search(key, value)
where("#{key} LIKE ?", "%#{value}%")
end
search("IS_ADMIN == 1 --", "")
Whoops!
The better way to do this would be to use Arel tables.
def search(key, value)
column = Model.arel_table[key.to_sym] # index into the columns, via a symbol
where(column.matches("%#{value}%"))
end
This cannot produce a SQL injection.
Try like this
def search(key,value)
where("#{key} LIKE ?","%#{value}%")
end

Rails/Postgres query Hstore for presence

I have some HStore columns in my Rails app. Looking around the Posgresql documentation and some blog posts, I've discovered that, given an HStore value that is a hash like this:
{ some_key: 'value' }
I can query the columns like so:
Model.where("the_hstore_column -> 'some_key' = 'value'")
There are a bunch of issues with this as a sufficient querying tool, however:
It really only makes sense for super simple values. If the value is
itself a hash or something, I have no idea how to search for it
effectively, if I don't know all its contents. (Even if I did, I'd
have to turn them all to stringified JSON or something).
It isn't helpful (at least, I can't make it helpful) for doing
queries for the presence or non-presence of the column's content (or
the content of any key under the column).
In other words (in pseudo-sql), I'd love to be able to do this:
Model.where("the_hstore_column = {}")
Model.where.not("the_hstore_column = {}")
Or:
Model.where("the_hstore_column -> 'some_key' = NULL")
Model.where.not("the_hstore_column -> 'some_key' = NULL")
Or best yet, given an HStore of value { some_key: { sub_key: 'value' } }:
Model.where("the_hstore_column -> 'some_key' INCLUDES (?)", 'sub_key')
Model.where.not("the_hstore_column -> 'some_key' INCLUDES (?)", 'sub_key')
These appear not to be working, but for the life of me, I can't find great information on how to conduct these queries. Does anyone know how to write them, or where I could look for better information?
UPDATE After more looking around, I found this post, which looked promising, but I can't get the code to actually work. For example Model.where("the_hstore_column ? :key", key: 'some_key') is returning an empty relation, even if there are many Model objects with some_key in the_hstore_column.
As requested by Andrius Buivydas, I'm pasting the relevant portion of my model below. It's relatively brief, because I decided to abstract out the Hstore-accessing into a module I wrote, essentially turning it into a hash store (which I though, apparently incorrectly, was its whole purpose). I tried using the built-in store_accessor, but it didn't work to my liking, (didn't seem to help parse saved hashes, for now obvious-seeming reasons) thus the ActiveRecord::MetaExt::HstoreAccessor module below. (ParsedHstore just takes an HStore string and parses it into a hash).
class Place.rb
include ActiveRecord::MetaExt::HstoreAccessor
hstore_accessor :hours, :extra
end
(In a separate file):
module ActiveRecord
module MetaExt
module HstoreAccessor
module ClassMethods
def hstore_accessor(*symbols)
symbols.each do |field|
class_eval do
define_method(field) do
ParsedHstore.new(self[field.to_sym]).hash_value
end
define_method("add_to_#{field}!") do |arg|
self[field.to_sym] = self[field.to_sym].merge(arg)
send("#{field}_will_change!")
save
arg
end
define_method("add_to_#{field}") do |arg|
self[field.to_sym] = self[field.to_sym].merge(arg)
send("#{field}_will_change!")
arg
end
define_method("remove_from_#{field}") do |arg|
other = self[field].dup
other.delete(arg.to_s); other.delete(arg.to_sym)
self[field.to_sym] = other
send("#{field}_will_change!")
self[field.to_sym]
end
define_method("remove_from_#{field}!") do |arg|
other = self[field].dup
other.delete(arg.to_s); other.delete(arg.to_sym)
self[field.to_sym] = other
send("#{field}_will_change!")
save
self[field.to_sym]
end
define_method("set_#{field}") do |arg|
self[field.to_sym] = arg
send("#{field}_will_change!")
self[field.to_sym]
end
define_method("set_#{field}!") do |arg|
self[field.to_sym] = arg
send("#{field}_will_change!")
self[field.to_sym]
end
end
end
end
end
def self.included(base)
base.extend ClassMethods
end
end
end
end
The idea is that this lets me easily add/remove values to an HStore field, without having to think about the merging and _will_change! logic every time. So I could do this, for example: Place.first.add_to_extra({ key: 'value'}).
Should I have made these fields json or jsonb? I feel like I'm reinventing the wheel here, or, more aptly, trying to turn a horse into a wheel or something.
Also, I may be misunderstanding the query example. I literally tried this query in my database (which has many places with a non-empty ranking key under the extra field), and turned up with an empty relation:
Place.where("extra ? :key", key: 'ranking')
I wouldn't be surprised if I messed this syntax up, as it seems really strange. Wouldn't that replace the ? with 'ranking', turning the query into this?: Place.where("extra ranking :key")? Seems weird and emphatically different from any other SQL I've run. Or is it turning to Place.where("extra ? ranking")? But ? is usually for safe injection of variables, no?
Please let me know if something else in my model or elsewhere would be more relevant.
It really only makes sense for super simple values. If the value is itself a hash or something, I have no idea how to search for it effectively, if I don't know all its contents.
Postgresql HStore stores key, values pairs that are both strings, only. So you can't store a hash, a nil or something else like an object - they will be converted to the strings.
Model.where("the_hstore_column ? :key", key: 'some_key')
That should work if everything is defined correctly. Could you paste an extract of the model file with the definition of the hstore column values?
Another way to find empty hstore columns is
Model.where(hstore_column: ['',nil])
OR
Model.where("hstore_column='' OR hstore_column IS NULL")

Rails - Conditional Query, with ActiveRecord?

Given a query like:
current_user.conversations.where("params[:projectid] = ?", projectid).limit(10).find(:all)
params[:projectid] is being sent from jQuery ajax. Sometimes that is an integer and the above works fine. But if the use selects "All Projects, that's a value of '' which rails turns into 0. which yields an invalid query
How with rails do you say search params[:projectid] = ? if defined?
Thanks
I think you may have mistyped the query a bit. "params[:projectid] = ?" shouldn't be a valid query condition under any circumstances.
In any case, you could do some sort of conditional statement:
if params[:project_id].blank?
#conversations = current_user.conversations.limit(10)
else
#conversations = current_user.conversations.where("project_id = ?", params[:project_id]).limit(10)
end
Although, I'd probably prefer something like this:
#conversations = current_user.conversations.limit(10)
#converstaions.where("project_id = ?", params[:project_id]) unless params[:project_id].blank?
Sidenotes:
You don't have to use .find(:all). Rails will automatically execute the query when the resultset is required (such as when you do #conversations.each).
Wherever possible, try to adhere to Rails' snakecasing naming scheme (eg. project_id as opposed to projectid). You'll save yourself and collaborators a lot of headaches in the long run.
Thanks but if the where query has lets say 3 params, project_id, project_status, ... for example, then the unless idea won't work. I'm shocked that Rails doesn't have a better way to handle conditional query params
EDIT: If you have multiple params that could be a part of the query, consider the fact that where takes a hash as its argument. With that, you can easily build a parameter hash dynamically, and pass it to where. Something like this, maybe:
conditions = [:project_id, :project_status, :something_else].inject({}) do |hsh, field|
hsh[field] = params[field] unless params[field].blank?
hsh
end
#conversations = current_user.conversations.where(conditions).limit(10)
In the above case, you'd loop over all fields in the array, and add each one of them to the resulting hash unless it's blank. Then, you pass the hash to the where function, and everything's fine and dandy.
I didn't understand why you put:
where("params[:projectid] = ?", projectid)
if you receive params[:project] from the ajax request, the query string shouldn't be:
where("projectid = ?", params[:projectid])
intead?
And if you are receiving an empty string ('') as the parameter you can always test for:
unless params[:projectid].blank?
I don't think i undestood your question, but i hope this helps.

Resources