I am doing some plain SQLs in my rails model (for purists this is just for complex SQLs :)
Since I am not using find*/condition methods, is there a helper method that I can use straight to do that?
The quote method on the connection object escapes strings. When building up queries, use sanitize_sql_for_conditions to convert ActiveRecord conditions hashes or arrays to SQL WHERE clauses.
The methods in ActiveRecord::ConnectionAdapters::DatabaseStatements are handy for direct queries, in particular the ones starting with select_.
Rails uses sanitize_sql_for_conditions internally for dealing with placeholders. Of course, that method is protected so you can't (cleanly) use it outside of an ActiveRecord model. You can get around the protectedness using send:
escaped_string = Model.send(:sanitize_sql_for_conditions, [
'id = ? and name = ?',
params[:id], params[:name]
]
)
Related
I have a GET API url something like: /api/countries.
I want to search from it using multiple parameters like name& code... For that, I will do /api/countries?name=pak&code=92
But what if I want to use OR in url params e.g. /api/countries?name=pakORcode=92
If you use OR as you have shown in your question then it will not work (name will contain the value pakORcode=92). If this is something that you would consider doing then you could use explode to split into key-value pairs around each OR.
You could maybe also have a third variable, method. You could set this to either AND or OR, then you'd know what method to use?
Hope this helps
What you describe is not a regular API call.
you can solve this in 2 ways
Create the logic in your controller where you implement the or statement
Implement the ODATA REST protocol.
I would prefer ODATA. Entity framework has support for this and you can even implement this on your own created lists.
In ODATA you can use filter expressions:
filter=name eq 610 or code eq 615
https://msdn.microsoft.com/en-us/library/hh169248(v=nav.90).aspx
While designing the API with with the team a suggestion was forwarded in regards to some complex query parameters that we sent which need to be encoded as objects, arrays of objects, etc. Suppose I have a route GET /resource/ and I want to apply a set of filters directly in the query params. The object literal structure of this filter would be something like
filter: {
field1: {
contains: 'value',
notin: ['value2', 'value3']
},
field2: {
greaterThan: 10
}
}
Encoding this in the url, via a query string parser such as the qs node module that express.js uses internally, would be cheap on the backend. However 1) The generated url is very hard to read, if a client wants to connect with the API he would need to use an encoding library and 2) I don't think I ever encountered the use of query params like this, it looks a little bit of overengineered and I'm not sure how used it is and if it is really safe.
The example above would yield query params such as:
GET /resource/?field1%5Bcontains%5D=value&field1%5Bnotin%5D%5B0%5D=value2&field1%5Bnotin%5D%5B1%5D=value3&field2%5BgreaterThan%5D=10
Does this practice of sending url query parameters that happen to be complex objects have some standards or best practices?
We implemented a different solution for filtering, when the list of possible parameters was very long. We ended up doing it in two steps, posting the filter and returning a filter ID. The filter ID could then be used in the GET query.
We had trouble finding any best practices for this.
In rails source (https://github.com/rails/rails/blob/fe4b0eee05f59831e1468ed50f55fbad0ce11e1d/activerecord/lib/active_record/sanitization.rb#L112) there is a sanitize_sql_like method that (I am hoping) will sanitize strings before using them with a SQL LIKE
however, I can't seem to use that, as Rails says that method doesn't exist.
My string has an apostrophe in it and the query is
#query = "Joe's"
Model.where("lower(field) LIKE ?", "%#{#query}%")
Using ActiveRecord::Base.sanitize doesn't help, as there are no results for the query.
How can I escape #query, and keep my SQL secured?
I've solved this same problem (on MySQL) using ActiveRecord::Sanitization::ClassMethods to properly sanitize user input. Unfortunately, the methods defined in ActiveRecord::Sanitization::ClassMethods are declared as protected, so they are only accessible in the scope of a Model class. The methods defined in ActiveRecord::Sanitization::ClassMethods are mixed in to all Model classes, so they are available directly from within the scope of a Model. That requires you to define a class/instance method or scope (ActiveRecord scope) on your model to use them, rather than using them externally, as in your example. However, design-wise, it's probably preferable to encapsulate the query logic in the Model anyway.
This solution also has the advantage of not only escaping the single-quote character, but also escaping other characters that would be interpreted by a SQL like query, such as the '%' character (among others). This should properly prevent SQL injection by escaping the the other characters which might be able to cause values to be interpreted rather than being treated as literals. There are also additional sanitization methods defined in ActiveRecord::Sanitization::ClassMethods that are useful for embedding user input (or other tainted input) in other SQL query contexts. Here is a working solution, tested on MySQL.
class Model < ActiveRecord::Base
# Finds a Model by case-insensitive substring match on Model.field
#
# #param query [String] A value to use in the substring match.
# #return [ActiveRecord::Relation] An `Relation` of `Model`s whose
# `field` includes the `query` substring.
scope :find_by_field_substring, ->(query) do
where(arel_table[:field].matches("%#{sanitize_sql_like(query)}%"))
end
end
You can then access this scope like this:
Model.find_by_field_substring "Joe's"
=> #<ActiveRecord::Relation [#<Model id: 1, field: "Joe's">]>
The usage of both ActiveRecord scopes and ActiveRecord::Relations are pretty well documented, but may only be available in newer versions of Rails.
Note that your database may require a second parameter to the sanitize_sql_like method to specify a different escape character than '\'.
Also note that escaping like this doesn't work for MySQL if it is running in NO_BACKSLASH_ESCAPES mode, because it forces a different mechanism for escaping values. If you're using NO_BACKSLASH_ESCAPES mode see this answer.
I haven't tested this part, but rather than using Arel, the SQL AST builder that sits underneath ActiveRecord, one could probably also use the style suggested in your initial example, as long as it is defined within the Model class.
class Model < ActiveRecord::Base
scope :find_by_field_substring, ->(query) do
where("lower(field) LIKE ?", "%#{sanitize_sql_like(query)}%")
end
end
If you use where properly, it will escape the input automatically
#query = "Joe's"
Model.where("lower(field) LIKE ?", "%#{#query}%")
Just note that your query is wrong. You have a lower() operator, then you pass an input which is not lower-case. The query will always return 0.
Moreover, lower() will reduce the ability of the database to use the index. In most databases, LIKE is already case insensitive (except for PostgreSQL where you should use ILIKE).
query = "Joe's"
Model.where("field LIKE ?", "%#{query}%")
or
query = "Joe's"
Model.where("field ILIKE ?", "%#{query}%")
Here's a real example on a real database. As you can see, the input is properly escaped in the final SQL.
> query = "Joe's"
> User.where("lower(email) LIKE ?", "%#{query}%")
User Load (4.4ms) SELECT "users".* FROM "users" WHERE (lower(email) LIKE '%Joe''s%')
=> #<ActiveRecord::Relation []>
Short and illegal answer:
ActiveRecord::Base.send(:sanitize_sql_like, text_to_escape)
Here's the output in the rails console (postgres)
irb(main):001:0> ActiveRecord::Base.send(:sanitize_sql_like, '%foo_bar%')
=> "\\%foo\\_bar\\%"
sanitize_sql_like is not a public method, so you should use it wisely.
You are not using this method just because of the apostrophe, it also escapes characters like %_ that are wildcards in Postgres.
This answer could be useful in case you want to extend ActiveRecord::Base, so all your models can include this method:
Rails extending ActiveRecord::Base
As previously stated, you can still use the sanitization methods if you use them within context of the model. Although adding a scope works (as shown in a previous answer), it's not strictly necessary. For example, you can add this to your model:
def self.where_ilike(search_terms)
where('search_tokens ILIKE ?', "%#{sanitize_sql_like(search_terms)}%")
end
#usersfound = User.find_by_sql(["
SELECT * from users where name ## plainto_tsquery('english', ?) LIMIT 20 offset ?
",#query,#offset])
See above, is this safe from sql injection? I am very new to doing direct sql commands on a database in rails. (I am aware there may be other ways of doing this SPECIFIC query, but I am wondering if in general, using find_by_sql and that kind of insertion of vars is safe - I have some difficult queries with subselects and joins that are really possible to do with ActiveRecord.
Thanks.
Yes, that should be safe. If you trace through the code you'll find that your find_by_sql call ends up calling PGconn#send_query_prepared with the bind parameters being carried along as little more than baggage; the send_query_prepared method is just a wrapper for the PQsendQueryPrepared API call in libpq:
static VALUE
pgconn_send_query_prepared(int argc, VALUE *argv, VALUE self)
{
/* ... bunch of boiler plate marshalling stuff ... */
result = PQsendQueryPrepared(conn, StringValuePtr(name), nParams,
(const char * const *)paramValues, paramLengths, paramFormats,
resultFormat);
/* ... */
}
The bind parameters end up in paramValues. So you should be fine unless there are bugs in PostgreSQL's C library prepared statement handling.
Inserting dynamic values into a query using query parameters is safe.
But it depends on whether Rails is "faking" query parameters, and is actually combining #query and #offset into the SQL string before preparing the statement. Then it's only as safe as the implementation of escaping in Rails.
I want to pass hashset to ActiveRecord finder method Model_name.where({ :key => value }). This works perfectly, but SQL composed from that uses straight comparison =. Is it possible to customize this and switch to LIKE comparison usage with hashset?
The :key => value syntax only works for =, IN, and BETWEEN conditions (depending on whether value is atomic, an Array, or a Range). Anything else requires you to pass the SQL as a string:
Model.where("key LIKE ?", value)