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