Rails is a mature web framework with a lot of improvements on security over time, but this doesn't mean it can totally protect your application from being hacked or compromised.
In this blog post, I will share some common mistakes that makes our Rails applications vulnerable.
Cross-site scripting (XSS)
Many Rails developers could inadvertently fall into this attack, mostly because of the popular misusage of raw
and html_safe
. First, let look at those familiar pieces of code:
<p><%== user.description %></p>
<p><%= raw user.description %></p>
<p><%= user.description.html_safe %></p>
They are dangerous because they don't remove or encode <script>
tags, so if someone try to submit description like this:
{
email: "john.doe@gmail.com",
name: "John Doe",
description: "<script>alert('Hello world!')</script>"
}
It won't be suprise if the browser show up an alert popup in the next time you access. And if you think alert('Hello world!')
is too boring, there're other serious things a hacker could do, like he/she can steal cookie data, load harmful script files, or redirect user to a phishing website.
{
description: "<script>alert(document.cookie)</script>"
}
{
description: "<script src='path/to/evil_script.js'></script>"
}
{
description: "<script>window.location = 'http://fake_gmail.com'</script>"
}
Lucky for us, Rails has a very helpful helper to help us deal with those situations, it's called sanitize
. When using sanitize
with default options, it will return similar result with html_safe
, except it will exclude all <script>
, <link>
and <style>
tags.
<p><%=sanitize user.description %></p>
<!-- "<span>Hello<br/>world</span><script></script>" -->
<p><span>Hello<br>world</span></p>
If we want to allow only some specific tags or attributes, we could also do that with sanitize
.
<p><%=sanitize user.description, tags: %w(br) %></p>
<!-- "<span>Hello<br/>world</span><script></script>" -->
<p>Hello<br>world</p>
Doing Javascript assignment inside a view template can also be exploited to perform XSS. For example:
<script>
var description = "<%== user.description %>";
// exploited with '";alert("Hello world!");"'
// exploited with '</script><script>alert("Hello world!")</script>'
</script>
Usually, using to_json
is enough to sanitize values before assigned for a Javascript variable.
<script>
var description = <%== user.description.to_json %>;
// var description = "\";alert(\"Hello world!\");\"";
// var description = "\u003c/script\u003e\u003cscript\u003ealert..."
</script>
But if your application requires this configuration:
ActiveSupport.escape_html_entities_in_json = false
Then to_json
is still exploitable.
<script>
var description = <%== user.description.to_json %>;
// exploited "</script><script>alert('Hello world!')</script>"
</script>
<!-- Result -->
<script>
var description = "</script><script>alert('Hello world!')</script>";
</script>
In this case, your best option is to use json_escape
helper (alias j
).
<script>
var description = "<%==j user.description %>";
// var description = "<\/script><script>alert(\'Problem?\')<\/script>";
</script>
SQL Injection
While ActiveRecord
do some good work to prevent SQL injection in its query interface, it still leaves door for that kind of attack with some methods such as joins
, select
, group
, etc. The reason is because those methods allow to pass any arbitrary string into the prepared sql statement.
One of the most common cases is using order
/ reorder
with a raw sql portion.
User.reorder("#{params[:sort_by]} #{params[:sort_dir]}")
An attacker could exploit this to get user's password character by character.
params[:sort_by] = "(CASE SUBSTR(password, 1, 1) WHEN 's' THEN 0 else 1 END)"
User.order("#{params[:sort_by]} #{params[:sort_dir]}")
Or he/she could go further, trying to break our system simply by doing this:
# MySQL
params[:sort_by] = "sleep(9999)"
User.reorder("#{params[:sort_by]} #{params[:sort_dir]}")
# PostgreSQL
params[:sort_by] = "pg_sleep(9999)::varchar"
User.reorder("#{params[:sort_by]} #{params[:sort_dir]}")
In order to prevent this attack, the best option is to explicitly whitelist all input from untrusted sources (browser, database, etc.). For example:
def whitelist_sort_fields
{
'fname' => 'customers.first_name',
'lname' => 'customers.last_name',
'bdate' => 'customers.birth_date'
}
end
def sort_field(sort_param)
whitelist_sort_fields[sort_param] || 'customers.first_name'
end
Another risk to SQL injection is the usage of ActiveRecord::Base.connection.select_all
or ActiveRecord::Base.connection.execute
because they will execute the sql statement without any sanitization step. Let take a look at the following sample:
raw_sql = "SELECT * FROM customers WHERE first_name='#{name}'"
ActiveRecord::Base.connection.select_all(raw_sql)
In this sample, the name
variable is injected directly into the raw sql statement, opening the door for hackers doing some nasty stuffs with the database. Luckily, there're some ways we could do to prevent it.
If we use ActiveRecord::Base.connection.select_all
/ ActiveRecord::Base.connection.execute
inside some class method of models,
we could utilize the protect method sanitize_sql
to sanitize the sql statement before passing it to ActiveRecord::Base.connection.select_all
/ ActiveRecord::Base.connection.execute
class Customer < ActiveRecord::Base
def self.get_raw_data(sql_array)
sql = sanitize_sql(sql_array)
connection.select_all(sql)
end
end
Customer.get_raw_data(
["SELECT * FROM customers WHERE first_name = ?", name])
If not, we should explicitly quote all values passed into the sql statement.
name = ActiveRecord::Base.connection.quote(name)
raw_sql = "SELECT * FROM customers WHERE first_name = #{name}"
ActiveRecord::Base.connection.select_all(raw_sql)
Insecure cookie
Most of web applications use cookie to store session data or id. Knowing this, hackers could try to steal or replicate cookie data in order to hijack users' session.
Therefore, keeping your website cookie secure is very important, and there're 2 options you must always consider to use for your sensitive cookie values, http_only
and secure
.
Setting http_only
to true
is to disable Javascript access to this cookie, so it can prevent hackers stealing sensitve data in cookie when your website is compromised by XSS.
The secure
option ensures that your cookie won't never be transmitted via nonsecure protocols. So if your application force all request to use https
, this option should be set to true
.
Recommended configuration for a Rails application in production should be:
# config/environments/production.rb
Rails.application.configure do
force_ssl = true
end
# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
key: '_my_app_session',
secure: Rails.env.production?,
httponly: true,
expire_after: 60.minutes
Furthermore, if your application has any sensitive cookie beside the session data or id, it should also be signed and set http_only
and secure
.
cookie.signed[:remember_me_token] = {
value: 'XXX',
expires: 1.day,
httponly: true,
secure: Rails.env.production?
}
Conclusion
In conclusion, there're values from untrusted sources such as request params or database that we should always remember to sanitize or whitelist before processing them.
More importantly, I hope this post would help you gain some awareness about security when working with your Rails applications.
Read more:
http://rail-sqli.com
http://brakemanscanner.org/
https://github.com/twitter/secureheaders
https://codeclimate.com