(Note: outdated text is marked in red. See “Update November 28” at the bottom of this post for details.)
Those who follow Ruby on Rails development are probably aware of the fact that Rails 2.0 will use a new session store by default, namely CookieStore.
Rails 1.2 defaults to PStore, which works as follows:
- Session data is stored in the server in files.
- The client is sent a unique, randomly generated session ID cookie, allowing the server to determine which session data file to use.
There are other session stores available for Rails 1.2 as well, such as MemCache store, MemoryStore, ActiveRecord store and SQLSessionStore. These stores work with session IDs as well, but store the session data in memcache, memory and database, respectively.
The reason why the Rails core developers introduced CookieStore is because PStore is known to cause performance problems and deadlocks. Furthermore, PStore requires the system administrator to clean up outdated session files once in a while. Michael Koziarski said: “So almost every prodution rails application has already switched to active_record_store or some other alternative and their apps will continue to use this alternative store.”
CookieStore on the other hand has the following benefits compared to PStore:
- Much faster.
- Nothing is saved on the server: all session data is stored in the client, so no cleanups of old session data is necessary.
- No deadlock is possible because no shared resources are accessed.
CookieStore is implemented by serializing session data into a cookie. Here’s an example of such a cookie:
BAh7BzoMdXNlcl9pZGkKIgpmbGFzaElDOidBY3Rpb25Db250cm9sbGVyOjpG%250 AbGFzaDo6Rmxhc2hIYXNoewAGOgpAdXNlZHsA–be9c1e802c6cf126c722c680 02ccbd5684a96dd9
The part before the “–” is the Base64 encoding of the serialized data. The part after is an HMAC. This HMAC is generated based on the serialized data, and a server-side secret.
This has huge security implications. If an attacker is able to determine the secret, then he can forge session data in any way he likes. Consider the following scenario’s:
- Session information is practically viewable as plain text. Any reasonably skilled developer can decode the cookie and see what is stored inside the session. So if you store, say, passwords in the session, then you’re screwed.
- Most Rails apps store a user_id field in the session, so an attacker can forge his login identity to that of an admin’s, by changing this field.
- It becomes hard to properly timeout a session, because the session time stored in the session cannot be trusted.
- People have pointed out that session replay attacks are possible.
The current session stores (PStore, ActiveRecord store, etc.) have none of these problems because they all work with session IDs. It should not be surprising that the introduction of CookieStore has generated a lot of controversy. My first reaction, too, was “WTF? Are they out of their minds?”.
To this date, the controversy remains. I think that there’s a lot of misinformation and fear floating around, so I hope to educate people with this post.
The Rails core team seems to think CookieStore is a good idea. I decided to do more research, and this is what I found:
- The problems with PStore are significant enough to warrant a replacement default implementation.
- It’s a bad idea anyway to store sensitive information (like passwords) in the session. It doesn’t matter whether the session data can be seen by the client or not, it’s something nobody should ever do.
- Forging session data is only possible if cracking the secret is feasible.
Points 1 and 2 are in my opinion very valid, so the only concerns left are point 3, and whether session replay attacks are possible.
Is it feasible to crack the scret?
According to Heiko Webers and Corey Benninger, the secret can be locally brute forced (which is certainly true), but they make it sound like it’s an easy thing to do. Benniger wrote a Ruby script for brute forcing the secret. On my machine, the example cookie provided by Webers is cracked within 1 second (the secret is “foo”). But this is a very insecure password anyway.
The blog posts written by Webers and Benninger are a bit outdated. According to them, Rails only checks whether the secret is blank. This is not true: in the latest implementation, Rails forces the secret to be at least 30 characters long. Furthermore, the Rails app generator creates a long, unique secret: an MD5 in hexadecimal format which is generated as follows: (UPDATE: the code below is outdated)
md5 = Digest::MD5.new
now = Time.now
md5 < < now.to_s
md5 << String(now.usec)
md5 << String(rand(0))
md5 << String($$)
md5 << @app_name
So by default, Rails apps will have a secret that’s
128/8*2=32 characters long, containing both alphabetical and numerical characters. A brute forcer will have to try
16^32=3.403*10^38 possible secrets in the worst case, and perhaps
16^32/2=1.701*10^38 possible secrets in the average case.
I’m not sure whether there are faster ways to crack the secret. I have heard that there’s an algorithm called SmartForce, which can brute force passwords much faster. But according to the description page of SmartForce, it doesn’t work with passwords that contain digits, so the default secret generated by Rails is safe from that algorithm.
I wrote my own brute forcing program just to see how fast my machine can brute force passwords. This program is written in C for raw number crunching speed. It is multithreaded, so it can take advantage of your dual/quad core machines. Use it as follows:
tar xzvf crackrailscookie.tar.gz cd CrackRailsCookie make ruby GenerateSessionCookie.rb john -i --stdout | ./CrackRailsCookie
It requires a POSIX environment (e.g. Linux, MacOS X, Cygwin) and that John the Ripper is installed.
GenerateSessionCookie.rb generates a Rails session cookie data and digest file, based on the session data and secret defined in its source code. CrackRailsCookie will try out passwords fed by John the Ripper, until it has found the correct one. You can specify the number of threads used by CrackRailsCookie, by specifying it as the first argument, i.e.
john -i --stdout | ./CrackRailsCookie 5
will spawn 5 threads.
My machine, an Intel Core 2 Duo T5300, can try 285840.1 passwords per second with 2 threads. To crack the default secret specified in GenerateSessionCookie.rb, “449fe2e7daee471bffae2fd8dc02313d”, my machine will have to run for
16^32/2 / 285840.1 = 5.952*10^32 seconds = 1.886*10^25 years
Ouch, I don’t think the universe will even last that long if we keep observing it.
However, Jamie Flournoy has pointed out that there are flaws in Rails’s secret key generator. See his comment below. The generator is based on the session ID generator in Ruby’s CGI::Session module. Upon investigation, it turns out that CGI::Session’s session ID generator uses a method similar to the one used in Netscape’s SSL implementation in 1995 – which was cracked! So this seems to make it computationally feasible for a skilled attacker to brute force the secret.
Session replay attacks
I don’t think session replay attacks are something that we have to worry about. At least, when compared to the other session stores out there.
An attacker can make himself logged in indefinitely by never expiring the session cookie. But the current session stores have this problem as well. The only way to fight this is to check the session’s expiration date in the server. The session will have to contain an ‘expires_at’ or ‘created_on’ field of some sort. If it is computationally infeasible to forge session data (i.e. if it is infeasible to crack the secret), then we can be sure that the ‘expired_at’/’created_on’ field can be trusted. If so, then session expiration is not a problem.
Another way an attacker can do damage, is by sniffing the session cookie of a different user, and then using that to forge his identity. However, this is the same as sniffing that user’s session ID, or sniffing the password that that user sends over his network. So at this aspect, CookieStore is not any less secure than all the other session stores. If people can sniff your network data then you’re screwed anyway. Better use SSL for the really important stuff.
If the application stores important, non-volatile-natured state information in the session, like ‘bank_account_balance’, then yes, CookieStore is vulnerable to a session replay attack. However:
- The application should have implemented server-side session expiration checking. So a replay attack will only work temporarily. If the server didn’t implement this check then it’s already fundamentally insecure no matter what session store it uses.
- It’s a bad, bad, bad idea to store such a thing in the session anyway. Suppose the user’s ISP suddenly gets attacked by a swarm of Zerglings and Hydralisks, and the user is only able to regain Internet access after a week. His session will have expired and he will find out that his bank account balance is 0! Even if the session store is not vulnerable to replay attacks, the application is still broken. Fixing the application happens to fix the vulnerability as well.
In the initial version of this blog post, I concluded that, if given a sufficient complex secret, forging the session data is computationally infeasible. Jamie Flournoy’s comment (see below), as well as this page, turned the tide. I now believe that CookieStore is insecure by default, but the only reason why that is so is because the secret key generator in Rails is flawed. This is something that can be fixed. I believe that, once the secret key generator has been fixed, CookieStore will be secure by default.
Rails forces the secret to be at least 30 characters long, so I don’t think we have to worry about uninformed developers inputting an insecure password. And although the client can see the content of the session, putting sensitive information in the session is a bad idea anyway. The inability to forge session data prevents makes session replay attack problems not any more severe than the other session stores.
So I agree with the Rails core team and welcome CookieStore as the default session store, provided that they fix their secret key generator.
Update November 25: Added information on session replay attacks.
Update November 26: I made a mistake in calculating the size of an MD5-based secret. This is fixed now.
Update #2 November 26: Updated this post with information on flaws in Rails’s secret key generator.
Update November 28: The key generation security problem has been fixed.