The Ruby on Rails and ClojureScript experts

Nov 15, 2010

The Firesheep Firefox plugin got a lot of attention lately, and rightly so. It raised awareness for the issue of HTTP session hijacking. It’s about time we make the internet a more secure place.

I just secured one of our soon to be launched products (Tiro) and am documenting the process here. When you follow this tutorial, you will get a Rails app fully secured against HTTP session hijacking as illustrated by the Firesheep Firefox plugin.

At the end of this tutorial, you will have:

  • A Rails app that uses SSL for every request (assets included)
  • Secure Rails and Authlogic authentication cookies that are only transmitted via SSL connections, so they are never sent in plain text

For this tutorial, we assume the following conditions:

  • Rails3
  • hosted on heroku (make sure your app already runs on heroku via http)
  • using heroku’s “Hostname Based Custom SSL” ($20.00 per month, NOTE: works on subdomains only)
  • all cookies marked as “secure”, even the authlogic persistence one
  • dev machine is OS X, however this is easily transferrable to other OSs

This is a pretty long procedure, so let’s start with a high level overview:

  1. Buy a certificate
  2. Set up the subdomain and certificate on heroku, update DNS
  3. Secure your Rails app by allowing SSL requests only via Rack middleware
  4. Mark Rails session cookies as “secure”
  5. Mark Authlogic persistence cookies as “secure”

For this post, I use my.tiroapp.com as an example subdomain. I assume that you want to lock down a single subdomain. Multiple subdomains or a top-level domain require slightly different procedures that are not covered here. So whenever you see my.tiroapp.com here, replace it with your own host name.

Alright, let’s start…

Buy a certificate

I bought mine for $13 at GoDaddy. To get that deal, simply google for “godaddy ssl”. Then click on the ad. This certificate is normally $50, so that’s a pretty good deal.

Once you have purchased it, you need to go to your GoDaddy dashboard and generate the certificate with the credit you purchased in the previous step.

When you start the process of generating a certificate, you’ll get to a point where the form asks for a CSR (Certificate Signing Request). Here is how to generate one:

  1. cd into your Rails app root
  2. > mkdir ssl-cert
  3. > cd ssl-cert
  4. > openssl genrsa -des3 -out host.key 2048
  5. enter and record a passphrase (we will remove it later, but we’ll need it again)
  6. > openssl req -new -key host.key -out host.csr
  7. answer all the questions, but be very careful to use “my.tiroapp.com” under the “organizational unit name” and “common name” fields

Here are example answers for the questions:

Country Name (2 letter code) [AU]:US
State or Province Name (full name) [Some-State]:California
Locality Name (eg, city) []:Mountain View
Organization Name (eg, company) [Internet Widgits Pty Ltd]: My Company Name
Organizational Unit Name (eg, section) []:my.tiroapp.com
Common Name (eg, YOUR name) []:my.tiroapp.com
Email Address []:contact@tiroapp.com

Please enter the following ‘extra’ attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

.

We have now generated a Certificate Signing Request. Let’s use it at GoDaddy:

  1. copy the contents of host.csr and paste them into godaddy’s form, where they ask for your certificate signing request.
  2. download the certificate from godaddy and unzip the two contained files into the ssl-cert folder in your rails app.
  3. combine the two files: cd into the ssl-cert folder
  4. > cat my.tiroapp.com.crt gd_bundle.crt > tiroapp_combined.crt
  5. > cat my.tiroapp.com.crt host.key > host.pem

Ok, now we have a certificate. In the next section we will remove the passphrase, as required by Heroku so that they can restart the server. Here you will need the passphrase I asked you to record earlier.

  1. cd into the ssl-cert folder in your rails app
  2. > openssl rsa -in host.pem -out nopassphrase.pem
  3. > openssl x509 -in host.pem >>nopassphrase.pem
  4. > openssl rsa -in host.key -out nopassphrase.key

With the passphrase removed, we can now set up things at heroku.

Set up the subdomain and certificate on heroku

Adding a “Hostname Based Custom SSL” to heroku costs $20 per month. The following commands will trigger that additional charge:

  1. cd into the rails root of your application
  2. heroku ssl:add ssl-cert/nopassphrase.pem ssl-cert/nopassphrase.key
  3. heroku addons:add custom_domains:basic
  4. heroku domains:add my.tiroapp.com
  5. heroku addons:add ssl:hostname

Once you are done with this, heroku will email you a CNAME to use for your subdomain. This CNAME has to be used instead of the regular proxy.heroku.com. Go to your DNS provider and create/update the CNAME for my.tiroapp.com. The new CNAME will be similar to:

appid1234herokucom-56789.us-east-1.elb.amazonaws.com

Secure your Rails app

Now that we have the certificate in place, we want to force all requests (app and assets) to use SSL. Rack is perfectly suited for this purpose:

# lib/middleware/force_ssl.rb
class ForceSSL
  def initialize(app)
    @app = app
  end

  def call(env)
    if env['HTTPS'] == 'on' || env['HTTP_X_FORWARDED_PROTO'] == 'https'
      @app.call(env)
    else
      req = Rack::Request.new(env)
      [301, { "Location" => req.url.gsub(/^http:/, "https:") }, []]
    end
  end
end

# config/application.rb
config.autoload_paths += %W( #{ config.root }/lib/middleware )

# config/environments/production.rb
config.middleware.use "ForceSSL"

Mark Rails session cookies as “secure”

We need to mark cookies as secure in order to prevent the browser from sending them in the clear if a user goes to “http” instead of “https” on your app. Your server will redirect the browser to the “https” URL, however any cookies that are not marked as secure will be transmitted in the clear with the original http request and are thus vulnerable.

# config/initializers/session_store.rb
TiroApp::Application.config.session_store(
  :cookie_store,
  :key => '_tiro_app_session',
  :secure => Rails.env.production?, # Only send cookie over SSL when in production mode
  :http_only => true # Don't allow Javascript to access the cookie (mitigates cookie-based XSS exploits)
)

# config/initializers/secret_token.rb
TiroApp::Application.config.secret_token = 'put a really long secret string here...'

Depending on what authentication library you use, your next steps might vary.

Mark Authlogic persistence cookies as “secure”

I use authlogic as authentication library for tiroapp.com. authlogic doesn’t mark its session persistence cookies as secure by default. So we will monkey patch it to do so:

# /config/initializers/authlogic_secure_cookie_patch.rb
#
# This patch makes authlogic's persistence cookie
# secure to prevent HTTP session hijacking
module Authlogic
  module Session
    module Cookies
      module InstanceMethods

      private

        def save_cookie
          controller.cookies[cookie_key] = {
            :value => "#{record.persistence_token}::#{record.send(record.class.primary_key)}",
            :expires => remember_me_until,
            :domain => controller.cookie_domain,
            :secure => Rails.env.production?, # Only send cookie over SSL when in production mode
            :httponly => true
          }
        end
      end
    end
  end
end

Whew, now your app is really secure!

Let me know if I left any security holes open (in regard to session persistence and communications security). Please note that in order to have your app really secure, there are a lot more things to consider. But that’s another blog post or two ;-).

Credits

  • Eric Butler for raising awareness for this issue with his Firesheep plugin
  • jmtame for a great walk through through the certificate process. I varied it in a number of places to adapt it to my needs
  • Simone Carletti for the ForceSSL rack middleware
  • bioneuralnet for the secure rails session cookie configuration