Building secure web applications with Ruby on Rails

This post was originally published on the New Bamboo blog, before New Bamboo joined thoughtbot in London.


The advantage of using a mature framework like Ruby on Rails to build your web applications is that it is reasonably secure by default. Current versions (4.0 and above) have several built-in mechanisms to stop opportunist hackers; the Rails community actively works to identify and patch new vulnerabilities as they come along; and, the framework is well documented both officially and unofficially. However, if some of the security features are disabled, your code doesn’t conform to best practises, or you don’t appreciate web security in general, your Rails apps can be left open to attack.

To understand this in more detail, earlier this year we took a day off from working on client projects to learn about web security in a workshop hosted by former Bambino, Najaf Ali. We spent the day attempting to complete challenges that involved exploiting vulnerabilities in typical web apps. Each of the apps for the workshop had been specifically tailored to contain some “secret” information that we had to find without looking at the source code, consulting any databases associated with the app, or indeed examining the web server logs.

One common way to attack a web app is via code injection, where the attacker introduces their own malicious code or parameters into the web application. This usually takes place wherever there is a means for data to be added to the web app, such as in a form or a POST request. Sometimes this data is not sanitised before being executed by the web server, entered into the backend database or shown again by the web app. This creates an opportunity for the attacker to insert some code which will get executed within the secure context of the web app.

Fortunately, Rails gives you some help in defending against code injection. Firstly, plain strings in your ERB views are automatically escaped,

<%= "<script>document.alert('Mwahahaha!')</script>" %>

becomes,

&lt;script&gt;document.alert('Mwahahaha!')&lt;/script&gt;

Furthermore, the sanitize helper can be used to strip unwanted HTML tags in your data,

sanitize("<script>document.alert('Mwahahaha!')</script>")
=> ""

You can also specify which tags you want to allow through,

sanitize("<strong>Safe</strong>\n<b>Dangerous</b>", tags: ['strong'])
=> "<strong>Safe</strong>\nDangerous"

And it’s clever enough to catch clever attackers. For example, the gsub method can be used to remove <script> tags from a string but it won’t completely sanitise the input,

"<sc<script>ript>".gsub("<script>", "")
=> "<script>"

but sanitize will,

sanitize("<sc<script>ript>")
=> "ript&gt;"

An extreme example of code injection is highlighted in the XKCD cartoon, Exploits of a mom. Here the attacker (mom) named her son “Robert'); DROP TABLE Students; --” (Little Bobby Tables) for the school’s records, knowing that if the unsuspecting school were to enter this in a query in their SQL database system, they would delete all the student records. There’s a more thorough explanation here. Fortunately, the find and find_by ActiveRecord methods automatically escape single quotes, double quotes, the NULL character and line breaks in SQL queries, disabling mom’s exploit. However, developers still need to be wary of any strings that are passed to query methods like where and find_by_sql, and manually sanitize them.

Another way to attack a web app is through the session cookie. The session cookie provides a useful way for the app to persist data across web requests, so it is a common place to record information unique to a user, like their ID. Fortunately Rails encrypts cookies by default so that session cookies cannot be read or altered by attackers unless they have the value of the secret_key_base parameter in the secrets.yml file.

However, an attacker who possesses a user’s encrypted session cookie may still potentially access the app posing as the user. This is known as session hijacking and can be mitigated by forcing the user to access the app via HTTPS instead of HTTP by setting,

config.force_ssl = true

in the application config file. Then the user’s session cookie (whether it is encrypted or not) won’t be distinguishable in the contents of the request, and subsequently won’t allow an attacker to “sniff” and copy it. In addition to this it is also important to force the user to clear their session cookie, in case they are accessing the app from a public terminal. The easiest way of doing this is to prominently display a “Log out” button in the app, as well as add reset_session in the action associated with logging the user out. An expiry time can also be allocated to session cookies with the use of a session controller,

class SessionController < ApplicationController
  def login
    if login_successful?
      reset_session
      session[:expires_after] = 1.hour.from_now.to_i
      redirect_to some_page_url and return
    end
    redirect_to new_login_url
  end

  def logout
    reset_session
    redirect_to root_url
  end

  private

  def login_successful?
    # check login is successful
  end
end

and then check the session hasn’t yet expired upon every request,

class ApplicationController < ActionController::Base
  before_action :check_session_validity

  private

  def check_session_validity
    unless session[:expires_after] && session[:expires_after] > Time.now.to_i
      redirect_to new_login_url
    end
  end
end

By understanding vulnerabilities like code injection and session hijacking, and knowing how Rails protects against them, we can have some confidence in the security of our web apps, and we can figure out how to fix them should they ever become compromised. Furthermore, it also teaches us why the safeguards are there, and ensure that if we ever disable them, we do so in a safe and controllable way.

I’ve only mentioned two common web security vulnerabilities in this post, but to learn about more the official Rails security guide is a great place to start. Also, the Open Web Application Security Project (OWASP) have created a cheatsheet for Rails, which is updated regularly, and the Ruby on Rails security mailing list is a good place to ask any questions.

There are additional tools that can be used in conjunction with Rails to ensure that your web app is not compromised. In particular, Brakeman is a static analysis tool that can identify vulnerabilities in Rails apps, and can be run as part of your continuous integration test suite. The SecureHeaders gem can automatically apply several security related headers to your app requests and responses. Also, Cloud Flare provides an easy way to protect your websites from online threats such as SQL injection or denial of service attacks.

If you want to understand and exploit vulnerabilities in web apps in a safe and instructive way, you should try the The Matasano Crypto Challenges. They are similar to the challenges we attempted with Najaf Ali but are not specific to Rails. Najaf’s workshop helped us realise that web security is everybody’s job but it is a considerably easier job when you’re using Ruby on Rails.