Rails Security Pitfalls
Rails comes with a lot of good security standards by default, but there are also some common pitfalls, less known methods, and details that one must take into account to create a secure app.
We’re going to take a quick dive into those pitfalls and see how to prevent them.
Public Secret Token
When creating a new app, Rails generates a random secret_token
used to verify the integrity of the session cookie. This sounds good, so what’s the problem?
Well, the secret_token
is in a file in our code, which means there are a lot of chances for it to be in our version control. We wouldn’t like a hacker (or an angry ex-coworker!) that accessed our code to also have access to any credentials or info that can give him more control.
In Rails, cookie data is deserialized using Marshal.load
, which can execute arbitrary ruby code. That means, if an attacker knows our secret_token
, he could easily craft a valid cookie with any data he wants and could execute some nasty code on our servers.
How To Prevent It
Fortunately, fixing this issue is pretty simple. Just use an ENV
variable for the secret_token
. That way you can set one in production, and every developer can have their own.
To do this, you will have to change the line in config/initializers/secret_token.rb
to:
MyRailsApp::Application.config.secret_token = ENV['SECRET_TOKEN']
And remember to set an ENV
variable before starting your app, or it will raise an error.
Tip: For local development, you can have an .env
that will be automatically loaded by tools like foreman or dot-env. It’s a good idea to have a env.example
file checked in to your version control so it’s easy to know which ENV variables are needed. If you’re using pow you can place it in your .powenv
too.
Inflections in the Wild
ActiveSupport::Inflectors
provides some really handy methods to deal with strings. For example, to get an object from a string you can do:
"Hash".constantize #=> Hash
It’s very common to get class objects from params with it, e.g.:
klass = params[:class].constantize
That’s riskier than it looks. An attacker could take advantage of that piece of code and check if we’re using a certain gem or requiring a certain lib, just by performing a request to that action with the class param (eg: /faulty/action?class=Mysql2
). If the class or module is loaded, the app will probably respond with a HTTP status 200, and with a 500 if it’s not.
Even worse, if the code also has something like this:
object = klass.new(params[:name])
A correct set of params like { class: 'File', name: '/etc/hosts' }
could expose our file system to attackers, or allow the execution of arbitrary code.
This cannot only happen with constantize
, but also with many other ActiveSupport::Inflector
methods that can be used to obtain a table or partial name.
How To Prevent It
Every time you use a pattern like the ones above think twice to see if you’re not exposing too much.
In such cases, you may want to whitelist some valid classes or parameters to prevent unwanted code execution or information leakage.
Logging Parameters
Not only can your app expose vulnerabilities, your logs can too. Yes, those helpful and seemingly harmless logs can contain sensitive data.
By default, Rails filters any parameter that matches the regular expression /password/
from being logged, replacing it with [FILTERED]
.
That’s a really good default, but you may want to add any other sensitive parameter to that filter.
How To Prevent It
Add any sensitive parameter name (e.g.: token, code, maybe email?) to your config/application.rb
file. The default file config looks something like this:
# Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters += [:password]
That filter can even work with a Proc
or a Regexp
. Strings and symbols are automatically converted to regular expresions that will match any part of the parameter name.
You can also add filters per environment. Just add a similar line to said environment initializer.
XSS
Rails does a really good job of preventing Cross-Site Scripting out of the box. The security guide explains the issue in depth.
Since Rails 3, all output to a view is escaped by default. That’s great, but there are still a lot of common occasions where XSS vulnerabilities may be introduced.
Examples
Say you want to create a helper that wraps some text in an HTML tag. You may be tempted to do something like:
def emphasize(text)
"<em>#{text}</em>".html_safe
end
The html_safe
call is needed in order for the view to correctly render the tags. But inadvertently we introduced a XSS vulnerability: Anything in the
text
argument would be treated as html_safe
too. That means that if text
was something like <script>alert("oops")</script>
, the resulting output would be <em><script>alert("oops")</script></em>
. The user won’t see the content and the script tag would be executed by the browser. That’s not what we’d like to happen.
To prevent that there are a couple options. You can explicitly escape the text
variable:
def safe_emphasize(text)
"<em>#{h text}</em>".html_safe
end
Or you could use the Rails’ content_tag
helper:
def another_safe_emphasize(text)
content_tag :em, text
end
In both cases, the content would be escaped, and the output will be <em><script>alert("oops")</script></em>
, which will display the text to the user and the script won’t be executed by the browser.
A Less Common Case
Another pitfall is using user-provided data in the href
part of the link_to
helper. For example:
<%= link_to "Website", user.website %>
In that case, if the user sets something like javascript:alert("oops")
as his website attribute, that javascript code would be executed. Unfortunately, fixing this case isn’t as simple. No built-in helper removes the javascript:
code.
How To Prevent It
- When creating HTML tags from your helpers, always try to use the built-in
content_tag
helper. - If for some reason you need to use strings, take a look at some helpful methods like
html_escape
andstrip_tags
. - Always double check if you’re using user-supplied data as a link’s
href
attribute. You may want to create some basic helper or validation to prevent vulnerabilities.
Denial of Service
A recent vulnerability was discovered in Rails that converted the keys of a hash into symbols when used as the find value for a query. That means that in a query like:
User.where(:name => { 'foo' => 'bar' })
the string ‘foo’ will be converted to a symbol.
A Quick Explanation
In Ruby, symbols are never garbage collected after they are created. A symbol always points to the same memory address — that’s what makes them so good for repeated hash keys. They don’t use new memory or have to be garbage collected!.
But that’s a double-edged sword. If too many symbols are created they may exahust physical memory.
Back to DoS
Fortunately, the example above was fixed for Rails versions newer than 3.2.12 (and the latest patch version of 2.3.x and 3.1.x). But that doesn’t mean that our application is immune to this type of attack.
If we are converting user input to symbols, a carefully crafted request can cause our app to create a huge amount of symbols, leading to a denial of service.
How To Prevent It
- Avoid usage of
to_sym
on user-supplied input. If there’s no better option, you may want to whitelist input before converting it to symbols. - It’s very common to see this kind of behavior in reporting controllers, pay special attention to those cases.
- Many libraries use
to_sym
under the hood, you may want to double check that before sending user-supplied input to them.
SQL Injection
This is one of the most common attacks. Fortunately, Rails does a pretty good job preventing it, and explaining it on it’s Security Guide.
Most of Rails’ built-in query methods perform sanitization before generating the SQL query. Most of them. That means there are a few that don’t. To make it more complicated, there are ways to avoid sanitization on the ones that do.
A simple case
For example, a query like
@posts = Post.where(user_id: params[:user_id])
does sanitize its params. But a distracted developer can write something like this:
@posts = Post.where("user_id = '#{params[:user_id]}'")
Everything will work as expected, but a vulnerability has just been introduced. ActiveRecord won’t escape that part of the query, letting an attacker use a well-crafted user_id
parameter to access all the posts records (this could be really bad if you’re dealing with sensitive information). A user_id
parameter of ' OR 1 --
would produce this query:
SELECT * FROM accounts WHERE user_id = '' OR 1 --'
It will match all records and the attacker would have access to them.
It’s always better to use hash conditions. If you need to use a string to set up some complex SQL condition, always use Rails’ interpolation:
@posts = Post.where("user_id = ? AND posted_at > ?", params[:user_id], params[:date])
That would work exactly as the first example, sanitizing the user_id
and date
parameter.
A Less Common Case
That was a relatively simple scenario. But the real pitfall are those methods that don’t sanitize their arguments and don’t document it.
Here’s an extensive list of those methods. It even points out several things to have in mind with the ones that do sanitize.
For example, the lock
method accepts any sql as it’s argument.
User.where(id: params[:id]).lock(params[:lock])
If an attacker provides a well crafted lock
parameter like " OR 1=1"
, the app would execute a query like this one:
SELECT `users`.* FROM `users` WHERE `users`.`id` = 1 OR 1=1
That would return all users instead of the one with the asked id
.
These kind of attacks and the form of the harmful parameters can greatly vary between different database engines. Be sure to use the same database engine in all your environments, and read their documentation.
How To Prevent It
- First of all, be sure to read the Rails’ Security Guide and have in mind the methods listed here.
- Always try to use hash conditions, or use
?
-type interpolation in other case. Never supply just one string for query conditions. - Double check when writing an uncommon query. You may want to try submitting some SQL yourself to see if it’s escaped.
A Gem to Keep Us Safe
These are a couple of things to take into account when developing an app. Unfortunately, there are a lot of other vulnerabilities and security pitfalls in Rails apps.
To make our lifes a bit easier (or to give us more to worry about?) there is Brakeman. Brakeman is a security scanner for Ruby on Rails applications. It works by looking directly into the source code instead of interacting with the app. That has some pros and some cons that are well explained in their documentation.
Once you run brakeman
, it produces a nice report with a list of warnings with it’s confidence, file, type, and a description:
+------------+---------------------------------------------------------+----------------------+--------------------------------------------+
| Confidence | Template | Warning Type | Message |
+------------+---------------------------------------------------------+----------------------+--------------------------------------------+
| High | posts/show | Cross Site Scripting | Unsafe parameter value in link_to href ... |
| Weak | dashboard/index (DashboardController#index) | Cross Site Scripting | Symbol conversion from unsafe string ... |
+------------+---------------------------------------------------------+----------------------+--------------------------------------------+
Messages have been truncated here, but Brakeman produces really helpful warning messages, including file line and context.
Tip: I recommend outputting the report to an HTML file (via the -o report.html
option) to have the complete information.
Brakeman also has a Jenkins Plugin if you want to scan your code in your CI environment. This is a really helpful and simple way to keep an eye on security.
Summing Up
Rails comes with some good security defaults, but there are still a lot of things to know when securing an app. It’s a good idea to keep these things in mind an perform a manual or automated check regularly to prevent possible attacks.