Let’s see how session data is handled in Rails 3.2 .

If you generate a Rails application in 3.2 then ,by default, you will see a file at config/initializers/session_store.rb . The contents of this file is something like

Demo::Application.config.session_store :cookie_store, key: '_demo_session'

First thing this line is telling is to use cookie to store session information.

Second thing this line is telling is to use _demo_session as the key to store cookie data.

A single site can have cookies under different key. For example airbnb is using 14 different keys to store cookie data.


airbnb cookies

Now let’s see how Rails 3.2.13 stores session information.

In my 3.2.13 version of Rails application I added following line to create session data.

session[:github_username] = 'neerajdotname'

Then I visit the action that executes above code. Now if I go and look for cookies for localhost:3000 then this is what I see .

demo session

As you can see I have only one cookie with key _demo_session .

The cookie has following data.


Let’s open rails console and try to decipher this information.

content = 'BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJTgwZGFiNzhiYWZmYTc3NjU1ZmVmMGUxM2EzYmEyMDhhBjsAVEkiFGdpdGh1Yl91c2V

When the content is written to cookie then it is escaped. So first we need to unescape it.

> unescaped_content = URI.unescape(content)
=> "BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJTgwZGFiNzhiYWZmYTc3NjU1ZmVmMGUxM2EzYmEyMDhhBjsAVEkiFGdpdGh1Yl91c2V

Notice that towards the end unescaped_content has -- . That is a separation marker. The value before -- is the real payload. The value after -- is digest of data.

> data, digest = unescaped_content.split('--')
=> ["BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJTgwZGFiNzhiYWZmYTc3NjU1ZmVmMGUxM2EzYmEyMDhhBjsAVEkiFGdpdGh1Yl91c2V
GbjJ1TXZEU0swamxyWU09BjsARg==", "b5bcce534ceab56616d4a215246e9eb1fc9984a4"]

The data is Base64 encoded. So let’s unecode it.

> Marshal.load(::Base64.decode64(data))
=> {"session_id"=>"80dab78baffa77655fef0e13a3ba208a",

So we are able to get the data that is stored in cookie. However we can’t tamper with the cookie because if we change the cookie data then the digest will not match.

Now let’s see how rails matches the digest.

In order to create the digest rails makes of use of config/initializer/secret_token.rb . In my case the file has following content.

Demo::Application.config.secret_token = '111111111111111111111111111111'

This secret token is used to create the digest.

> secret_token =  '111111111111111111111111111111'
> OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get('SHA1').new, secret_token, data)
=> "b5bcce534ceab56616d4a215246e9eb1fc9984a4"

Notice that the result of above produces a value that is same as digest in earlier step. So if cookie data is tampered with then the digest match will fail. This is why it is absolute necessary that attacker should not be able to get access to secret_token value.

Did you notice that we can access the cookie data without needing secret_token. It means the data stored in cookie is not encrypted and anyone can see it. That is why it is recommended that application should not store any sensitive information in cookie .

In the previous example we used session to store and retrieve data from cookie. We can directly use cookie and that gives us a little bit more control.

cookies[:github_username] = 'neerajdotname'

Now if we look at cookie stored in browser then this is what we see.

update cookie

As you can see now we have two keys in our cookie. One created by session and the other one created by code written above.

Another thing to note is that the data stored for key github_username is not Base64encoded and it also does not have -- to separate the data from the digest. It means this type of cookie data can be tampered with by the user and the Rails application will not be able to detect that the data has been tampered with.

Now let’s try to sign the cookie data to make it tamper proof.

cookies.signed[:twitter_username] = 'neerajdotname'

Now let’s look at cookies in browser.

update cookies

This time we got data with another key twitter_username . Another thing to notice is that cookie data is signed and is tamper proof.

When we use session then behind the scene it uses cookies.signed. That’s why we end up seeing signed data for key _demo_session .

What happens when user tampers with signed cookie data.

Rails does not raise any exception. However when you try to access cookie data then nil is returned because the data has been tampered with.

Security should be on by default

session , by default, uses signed cookies which prevents any kind of tampering of data but the data is still visible to users. It means we can’t store sensitive information in session.

It would be nice if the session data is stored in encrypted format. And that’s the topic of our next discussion.

Rails 4 stores session data in encrypted format

If you generate a Rails application in Rails 4 then ,by default, you will see a file at config/initializers/session_store.rb . The contents of this file is something like

Demo::Application.config.session_store :cookie_store, key: '_demo_session'

Also you will notice that file at config/initializers/secret_token.rb looks like this .

Demo::Application.config.secret_key_base = 'b14e9b5b720f84fe02307ed16bc1a32ce6f089e10f7948422ccf3349d8ab586869c11958c70f46ab4cfd51f0d41043b7b249a74df7d53c7375d50f187750a0f5'

Notice that in Rails 3.2.x the key was secret_token. Now the key is secret_key_base .

session[:github_username] = 'neerajdotname'

cookies and site data

Cookie has following data.


Let’s open rails console and try to decipher this information.

content = 'RkxNUWo4NlBKakoyU1VqZWJIKzNaV0lQVVJwQjZhdUVTRnowVHppSVJ3Mk84TStoS1hndFZFNHlNaGw2RHBCc0ZiaEpsM0NtYTg4d

When the content is written to cookie then it is escaped. So first we need to unescape it.

unescaped_content = URI.unescape(content)
=> "RkxNUWo4NlBKakoyU1VqZWJIKzNaV0lQVVJwQjZhdUVTRnowVHppSVJ3Mk84TStoS1hndFZFNHlNaGw2RHBCc0ZiaEpsM0NtYTg4d

Now we need secret_key_base value. And using that let’s build key_generator .

secret_key_base = 'b14e9b5b720f84fe02307ed16bc1a32ce6f089e10f7948422ccf3349d8ab586869c11958c70f46ab4cfd51f0d41043b7b249a74df7d53c7375d50f187750a0f5'
key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
key_generator = ActiveSupport::CachingKeyGenerator.new(key_generator)

Our MessageEncryptior needs two long random strings for encryption. So let’s generate two keys for encryptor.

secret = key_generator.generate_key('encrypted cookie')
sign_secret = key_generator.generate_key('signed encrypted cookie')
encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)

Now we can finally decipher the data.

data =  encryptor.decrypt_and_verify(unescaped_content)
puts data
=> neerajdotname

As you can see we need the secret_key_base to make sense out of cookie data. So in Rails 4 the session data will be encrypted ,by default.

Rails4 will transparently will upgrade cookies from unencrypted to encrypted cookies. This is a brilliant example of trivial choices removed by Rails.