March 19, 2013
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 this.
Demo::Application.config.session_store :cookie_store, key: '_demo_session'
As we can see _demo_session
is used 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.
Now let's see how Rails 3.2.13 stores session information.
In 3.2.13
version of Rails application I added following line to create
session data.
session[:github_username] = 'neerajsingh0101'
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 .
As you can see I have only one cookie with key _demo_session
.
The cookie has following data.
BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJTgwZGFiNzhiYWZmYTc3NjU1ZmVmMGUxM2EzYmEyMDhhBjsAVEkiFGdpdGh1Yl91c2V
ybmFtZQY7AEZJIhJuZWVyYWpkb3RuYW1lBjsARkkiEF9jc3JmX3Rva2VuBjsARkkiMU1KTCs2dXVnRFo2R2NTdG5Kb3E2dm5Bcl
ZYRGJGbjJ1TXZEU0swamxyWU09BjsARg%3D%3D--b5bcce534ceab56616d4a215246e9eb1fc9984a4
Let's open rails console
and try to decipher this information.
content = 'BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJTgwZGFiNzhiYWZmYTc3NjU1ZmVmMGUxM2EzYmEyMDhhBjsAVEkiFGdpdGh1Yl91c2V
ybmFtZQY7AEZJIhJuZWVyYWpkb3RuYW1lBjsARkkiEF9jc3JmX3Rva2VuBjsARkkiMU1KTCs2dXVnRFo2R2NTdG5Kb3E2dm5BclZYRGJGbjJ1T
XZEU0swamxyWU09BjsARg%3D%3D--b5bcce534ceab56616d4a215246e9eb1fc9984a4'
When the content is written to cookie then it is escaped. So first we need to
unescape
it.
> unescaped_content = URI.unescape(content)
=> "BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJTgwZGFiNzhiYWZmYTc3NjU1ZmVmMGUxM2EzYmEyMDhhBjsAVEkiFGdpdGh1Yl91c2V
ybmFtZQY7AEZJIhJuZWVyYWpkb3RuYW1lBjsARkkiEF9jc3JmX3Rva2VuBjsARkkiMU1KTCs2dXVnRFo2R2NTdG5Kb3E2dm5BclZYRG
JGbjJ1TXZEU0swamxyWU09BjsARg==--b5bcce534ceab56616d4a215246e9eb1fc9984a4"
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
ybmFtZQY7AEZJIhJuZWVyYWpkb3RuYW1lBjsARkkiEF9jc3JmX3Rva2VuBjsARkkiMU1KTCs2dXVnRFo2R2NTdG5Kb3E2dm5BclZYRGJ
GbjJ1TXZEU0swamxyWU09BjsARg==", "b5bcce534ceab56616d4a215246e9eb1fc9984a4"]
The data is Base64
encoded. So let's unecode it.
> Marshal.load(::Base64.decode64(data))
=> {"session_id"=>"80dab78baffa77655fef0e13a3ba208a",
"github_username"=>"neerajsingh0101",
"_csrf_token"=>"MJL+6uugDZ6GcStnJoq6vnArVXDbFn2uMvDSK0jlrYM="}
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] = 'neerajsingh0101'
Now if we look at cookie stored in browser then this is what we see.
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] = 'neerajsingh0101'
Now let's look at cookies in browser.
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.
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.
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] = 'neerajsingh0101'
Cookie has following data.
RkxNUWo4NlBKakoyU1VqZWJIKzNaV0lQVVJwQjZhdUVTRnowVHppSVJ3Mk84TStoS1hndFZFNHlNaGw2RHBCc0ZiaEpsM0NtYTg4d
nptcjFaQWVJbUdOaFh5MVlCdWVmSHBMNWpKbkRKR0JrSU5KZFYwVjVyWTZ3aUNqSWxJM1RTMkQybEtPUFE5VDFsZVJyakx0dFh3PT
0tLTZ5NGIreU00Z0MyNnErS29SSGEyZkE9PQ%3D%3D--3f2fd67e4e7785933485a583720d29ba88bca15f
Let's open rails console
and try to decipher this information.
content = 'RkxNUWo4NlBKakoyU1VqZWJIKzNaV0lQVVJwQjZhdUVTRnowVHppSVJ3Mk84TStoS1hndFZFNHlNaGw2RHBCc0ZiaEpsM0NtYTg4d
nptcjFaQWVJbUdOaFh5MVlCdWVmSHBMNWpKbkRKR0JrSU5KZFYwVjVyWTZ3aUNqSWxJM1RTMkQybEtPUFE5VDFsZVJyakx0dFh3PT
0tLTZ5NGIreU00Z0MyNnErS29SSGEyZkE9PQ%3D%3D--3f2fd67e4e7785933485a583720d29ba88bca15f'
When the content is written to cookie then it is escaped. So first we need to
unescape
it.
unescaped_content = URI.unescape(content)
=> "RkxNUWo4NlBKakoyU1VqZWJIKzNaV0lQVVJwQjZhdUVTRnowVHppSVJ3Mk84TStoS1hndFZFNHlNaGw2RHBCc0ZiaEpsM0NtYTg4d
nptcjFaQWVJbUdOaFh5MVlCdWVmSHBMNWpKbkRKR0JrSU5KZFYwVjVyWTZ3aUNqSWxJM1RTMkQybEtPUFE5VDFsZVJyakx0dFh3PT 0tLTZ
5NGIreU00Z0MyNnErS29SSGEyZkE9PQ==--3f2fd67e4e7785933485a583720d29ba88bca15f"
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
=> neerajsingh0101
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.
If this blog was helpful, check out our full blog archive.