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.
Further Resources
- Official Rails security guide
- Official Rails Security Mailing List
- List of Rails’ query methods that don’t sanitize their arguments
- Railscasts on security
- How to hack a Rails app using its secret_token
- Rails Security course by CodeClimate
Frequently Asked Questions (FAQs) about Rails Security Pitfalls
What are some common Rails security pitfalls?
Rails security pitfalls are common mistakes or oversights that developers make when building Rails applications, which can lead to security vulnerabilities. These can include things like not validating user input, failing to use secure cookies, not properly managing user sessions, and not encrypting sensitive data. It’s important to be aware of these pitfalls and take steps to avoid them in order to build secure Rails applications.
How can I avoid Rails security pitfalls?
Avoiding Rails security pitfalls involves a combination of best practices and vigilance. Always validate user input to prevent SQL injection attacks, use secure cookies to protect user data, manage user sessions properly to prevent session hijacking, and encrypt sensitive data to protect it from unauthorized access. Regularly updating your Rails application and keeping abreast of the latest security vulnerabilities and patches can also help you avoid these pitfalls.
What is the role of the environment initializer in Rails security?
The environment initializer plays a crucial role in Rails security. It’s responsible for setting up the initial environment for the Rails application, including the security settings. By properly configuring the environment initializer, you can ensure that your Rails application has the right security settings from the start.
How can I use the environment initializer to improve Rails security?
You can use the environment initializer to improve Rails security by configuring it to enforce secure settings. For example, you can set it to use secure cookies, enforce HTTPS, and enable other security features. You can also use it to set up the initial environment for your Rails application, including the database and other resources, in a secure manner.
What are some common mistakes developers make with the environment initializer?
Some common mistakes developers make with the environment initializer include not properly configuring it for security, not using it to set up the initial environment in a secure manner, and not regularly updating it to incorporate the latest security patches and updates. These mistakes can lead to security vulnerabilities in your Rails application.
How can I avoid mistakes with the environment initializer?
Avoiding mistakes with the environment initializer involves understanding its role in Rails security and taking the time to properly configure it. Always use it to enforce secure settings, set up the initial environment in a secure manner, and regularly update it to incorporate the latest security patches and updates.
What is the role of secure cookies in Rails security?
Secure cookies play a crucial role in Rails security. They help protect user data by ensuring that cookies are only sent over secure connections. By using secure cookies, you can help prevent attacks that exploit insecure cookies, such as session hijacking.
How can I use secure cookies to improve Rails security?
You can use secure cookies to improve Rails security by configuring your Rails application to always use them. This can be done in the environment initializer or in the application’s configuration settings. By using secure cookies, you can help protect user data and prevent attacks that exploit insecure cookies.
What are some common mistakes developers make with secure cookies?
Some common mistakes developers make with secure cookies include not using them at all, not properly configuring them, and not understanding their role in Rails security. These mistakes can lead to security vulnerabilities in your Rails application.
How can I avoid mistakes with secure cookies?
Avoiding mistakes with secure cookies involves understanding their role in Rails security and taking the time to properly configure them. Always use secure cookies, understand how they work, and regularly update your knowledge about them to stay abreast of the latest security practices.
I’m a developer, maker, and entrepreneur from Buenos Aires, Argentina. Mostly working with Ruby and Javascript. I love to create and work on great products that solve real world needs, and believe every problem deserves a thoughtful solution.