The CookieStore session storage in Ruby on Rails is not new; in fact, it has been the default session store since Rails 2.0. Since then, there have been countless blog posts and forum threads discussing various security concerns vs a server-sided store (ActiveRecordStore, Memcache, SqlStore, etc.). They all seem to miss an important point: by default, a stolen cookie gives the thief a lifetime pass to a user account!
I will explain how this happens and some steps you can implement to mitigate it.
Most discussion seems to be focused very much on the obvious security difference: that data in the cookie is stored in plain text (well its stored in Base64, but that's trivially convertible). Storing sensitive information or stateful data in a session, be it server side or cookie store, is bad practice and for most of us storing simple reference information, like the logged in user's id, this is not usually much of a concern.
Lets use the example: our cookie stores the logged in user's ID or nothing if the user isn't logged in. This is a common scheme used in many of the rails screencasts and guide books.
Instead of storing a
session_id which identifies a server side storage record to get the user's ID, we store the user ID in the cookie. Generally this user ID isn't sensitive, its probably in the user's profile URL or within the code somewhere.
If you're following the above rule and only storing simple reference information, your cookie isn't much different than a server-sided store. The HMAC digest within the Rails CookieStore cookie prevents someone from tampering and changing the cookie, so we cannot change our cookie to a different user ID and be logged in as that user, same way it would be difficult to guess a different
session_id and access someone else's session.
Since both implementations rely on a cookie being passed anyways, they have the same security concerns, and are equally susceptible to replay and fixation attacks.
Ok, so what is our concern then?
There is one major difference that never seems to be brought up. If (and when!) a CookieStore cookie is stolen:
By default, a CookieStore session will never become invalid.
By this, I mean if I steal an authenticated Cookie, I can use it to access the site as that user. This is a common attack called Session Hijacking or Sidejacking but with server-sided storage it can be mitigated.
Generally, with a server side store, you delete the
session_id and accompanying data when a user logs out or times out. Any stolen
session_ids are no longer valid because that
session_id no longer exists. If the valid user never logs out and the attacker keeps sending requests, they can keep the session alive, but this can still be detected and stopped.
With the cookie based store, even if the valid user logs out and an expiry date is put on the cookie, an attacker can change the expiry date and replay the cookie at any time. It will always be valid as there is no
session_id to compare against and the cookie expiry is not guarded by the HMAC digest.
The Rails Guides suggest using
reset_session to stop hijacking, but this does not help for CookieStore, there is no session identifier to reset!
Give it a try yourself! On any of your CookieStore based rails app:
- Load up Firefox and install the Live HTTP Headers or Tamper Data plugin, i'll use Live HTTP Headers.
- Log into your app.
- Start Live HTTP Headers
- Go to a page that shows you if you are logged in or not.
- On the Live HTTP Headers modal, select the main request header, its usually the top most one. Pretend you are an attacker and read this off a wireless network.
- Log out of your web app.
- Back on the Live HTTP Headers modal, you are now the attacker logging in with stolen cookie: replay the main request header.
- You will now be logged into your app. No matter what you do, the attacker can save that stolen cookie and replay it any time from anywhere and log back in.
How can we stop it?
Well, stopping it is quite easy, and I'll explain a couple of ways how, but the real reason I bring this up is because it isn't obvious to people using the default session store there is a huge concern here. Everyone talks about how never to store sensitive information in the cookie, but getting an authenticated cookie, by default, gives you life-time pass to that user's account. That seems much worse! Without a security measure, your application has a huge hole, and none of the rails tutorials, documentation, or screencasts seem to mention this.
No matter what authentication library you use: Devise, Authlogic, or if you roll your own, they are all susceptible because they all use whatever session store you decide.
The best and easiest solution is simply to use SSL. Not just on your login forms and actions, but your entire site, or at least any pages where you have sessions turned on. With SSL on, the user will not be able to replay your cookies and the entire attack vector is shut down. Rails 3.1 has a handy
force_ssl switch you can use, and you can use something like:
:secure => Rails.env.production?
config.session_store declaration to ensure the cookie is only served over SSL.
If you don't or can't use SSL, try implementing a timeout and a nonce within the HMAC protected portion of the cookie.
For example, managing a session timeout yourself by updating the expiry date on every request unless the expiry has passed creates a non-editable timeout on the session and will invalidate it after a specific time. Of course the attacker can still touch your app to keep the timeout alive indefinitely, but it helps.
In addition to a timeout, adding a nonce, even something simple, can help invalidate existing cookies. Storing a hash based on the user's last login and/or logout time can invalidate stolen cookies every time the valid user logs in/out. This, coupled with the timeout can mitigate hijacking and puts it on par with server-based session management schemes.
Regardless of the session store mechanism you use, they're all susceptible to attack unless you're using SSL. Unfortunately, by default, Rails' CookieStore gives you a no-fuss lifetime pass instead of a day-pass.