Rails 2.0, cookie session store and security

(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:

  1. 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.
  2. 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.
  3. It becomes hard to properly timeout a session, because the session time stored in the session cannot be trusted.
  4. 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:

  1. The problems with PStore are significant enough to warrant a replacement default implementation.
  2. 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.
  3. 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)

Ruby
  1. md5 = Digest::MD5.new
  2. now = Time.now
  3. md5 < < now.to_s
  4. md5 << String(now.usec)
  5. md5 << String(rand(0))
  6. md5 << String($$)
  7. 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.

Conclusion

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.

20 Comments »

  1. josh susser said,

    November 25, 2007 @ 5:54 pm

    Thanks for the very clear explanation. I think this will help mollify the angry villagers. I hope to see this information in some form somewhere in the Rails API docs and also in the wiki. I added some very simple info about alternate session stores to the rdoc last week, but there should be something like this somewhere central too.

    While this is good information, you don’t anywhere mention the second-biggest security issue with CookieStore sessions, which is the session replay attack. This is partly covered by your third scenario (impossible to properly timeout a session), but is significant enough that it deserves its own discussion. With some basic safeguards in your session design you can protect against replay attacks, but you have to understand what the issue is and what are the tradeoffs. You clearly have a grasp of the issues, so it would be great to see what you have to say about it.

  2. Hongli said,

    November 25, 2007 @ 7:58 pm

    Thanks for pointing that out. I’ve updated the post with information on replay attacks, and my view on it.

  3. mla said,

    November 25, 2007 @ 11:57 pm

    I’ve been looking at the PStore issues too.

    One very simple change is to hash the directory structure, which makes performance much more linear on most filesystems:
    http://mlawire.blogspot.com/2007/11/ruby-cgisessionpstore-performance.html

    I posted a patch to ruby-core but it doesn’t look like there’s much interest.

    The other thing I discovered is that PStore uses an MD5 of the marshaled data to determine if a write is needed.
    However, marshal doesn’t make any attempt to order the data, so it’s very easy to end up with structures that
    flip-flop and always cause a write even if nothing changed:

    http://mla.homeunix.com/tmp/marshal-test.rb
    http://mla.homeunix.com/tmp/marshal-test.txt

    I also posted a note about that to one of the lists and mentioned that Perl’s serialization module, Storable, has a canonical option that orders hashes. Someone pointed out that hash keys aren’t necessarily comparable in ruby, so I’m not sure what the fix would be like. Obviously the ordering doesn’t need to make sense… it just needs to be consistent.

    Also, with a load balancer (such as Pound) that has session-affinity, you can easily scale a file-based session approach to any number of servers.

    I think the cookie-based sessions sound fine but I would like to see the file-based issues addressed.

  4. jc said,

    November 26, 2007 @ 4:32 am

    Wow. Fantastic Post. Very informative. But you forgot to mention a big drawback… cookie size. If you put in a (really) long flash message or start loading too much into session you’ll run into big problems.

    Just something we need to keep in mind. All I try to store in session ever is User ID and Flash messages. However even apps like Beast use session to store some basic caching stuff (like thread counts).

  5. Jamie Flournoy said,

    November 26, 2007 @ 10:17 am

    It’s much easier to brute force this than you think. The MD5 hash in this case will only generate about 2^62 values, or 10^13 possible secrets.

    Why? Well, the rand() function is not seeded in the AppGenerator code, so Kernel::rand uses current time and PID as a default seed value, according to the Ruby docs. PID is 16 bits in size; current system time with microsecond precision gives a bit less than 2^45 possible values per year. If you assume that a target Rails 2.0 app was generated during 2007 or 2008 (one more bit of uncertainty) you get a possible # of rand() output values of 2^62 (62 = 45 + 1 + 16). The code in AppGenerator tries to pad this with Time.now() and PID, but these randomness sources are already in the rand() output value since they were the same sources of randomness used in the seeding of the PRNG rand() used, so that actually accomplishes nothing.

    So when 2^62 values are MD5’d, you still get 2^62 values out, but each of those is now 128 bits long. No need to test 2^128 keys if you can just test 2^62 keys. So that’s 2^66 times easier than you estimated it would be to brute force. Eek!

    Using your script that would take 168 years for one computer to break, worst case. Get a few thousand zombified PCs working on it, and you can forge any session for that particular Rails app in a matter of months. Not trivial, but quite doable.

    I don’t see why they don’t just mandate that OpenSSL or /dev/urandom is used for the PRNG, generate a suitably large key from that (512 bits?), and symmetric-encrypt the whole darn cookie with that.

  6. Hongli said,

    November 26, 2007 @ 12:27 pm

    Jamie, you’re right. In fact I’ve found this page: http://epsilondelta.net/2006/05/17/examining-rubys-http-session-id-generation/
    The Rails app generator’s secret key generator is based on Ruby’s session ID generation code, so they’re both equally insecure.

    This is a problem, and should be fixed.

  7. josss said,

    November 26, 2007 @ 4:15 pm

    So an attacker can do damage by sniffing the session cookie of a different user and then using that to forge his identity. Although that’s the same case as other session stores, how is this typically handled? Is at least IP address matching checked? Securing this with expiration dates seems not enough…

  8. Hongli said,

    November 26, 2007 @ 5:26 pm

    Josss: It’s typically not handled. IP address checking might work, but:

    • There are a lot of users behind load balancers, and so they might change IP frequently. IP address checking breaks sessions for these users. I heard that this applies to many AOL users.
    • Theoretically, the attacker might be able to obtain the same IP. Many people have their IP assigned via DHCP.

    Such is the fundamental insecurity of unencrypted HTTP sessions. People still put up with HTTP sessions because they usually assume that:

    • randomly generated session keys are hard enough to guess
    • it’s unlikely that an interested attacker can actually sniff the network data
    • HTTP session keys, in combination with expiration, provides “good enough” security for most applications

    And they would usually be right. I’ve never actually heard of an evil hacker sniffing a MySpace user’s network data to get his login credentials. Most web applications just aren’t that important. Evildoers seem to typically steal peoples’ passwords by using trojans and malware, e.g. BitneySpearsNaked.jpg.exe.

    For the really serious stuff though, i.e. online banking and credit card payment processing, SSL is the only solution. SSL makes it impossible for an attacker to sniff the unencrypted network data.

  9. Rails 2.0 security « jos442’s blog said,

    November 26, 2007 @ 11:06 pm

    […] I’ve been takin a glance at Rails 2.0 new session storage. Hongli points out in his blog that cookies are not stored anymore as files in Rails 2.0. This makes the design more RESTful, as long as no state is kept in the server […]

  10. a work on process » links for 2007-11-27 said,

    November 27, 2007 @ 5:21 am

    […] 赖洪礼的 blog » Rails 2.0, cookie session store and security Looking at the security implications of the new session storage which becomes the default in Rails 2.0 (tags: cookies rubyonrails security sessions) […]

  11. Adam Fields said,

    November 28, 2007 @ 5:55 pm

    It may not seem like a lot of data, but the cookies get sent back to the server with every request, including every XMLHttpRequest. If you’re storing a lot of info in the session, your roundtrip times are going to start to “mysteriously” lag.

  12. Hongli said,

    November 28, 2007 @ 5:59 pm

    That is correct. That’s why it isn’t a good thing to store a lot of data in it (it never was in the first place). Though I think the session data size is neglible compared to the size of the average web page.

  13. Adam Fields said,

    November 28, 2007 @ 8:43 pm

    Yes, but not compared to the size of the average XMLHttpRequest call, which really depends on being as fast as possible.

  14. Sligo said,

    December 8, 2007 @ 6:25 am

    Ok Adam, you win. don’t use cookie based session storage with your ajax site. I won’t be using it either, and in general I am not a fan of sending cached data back and forth. seems wasteful to me. I assume the id generation will be patched, but even after that, i wont be able to sleep well at night knowing that i am bouncing session data back and forth between my clients and my servers simply because I suddenly have no regard for bandwidth… (Not a fan of viewstate either, but I would choose that in a heartbeat over this)

  15. Pascal said,

    December 18, 2007 @ 2:30 pm

    Thanks for the detailed information!

    “Is session based cookie store a good thing?”: as usual in CS the answer is “it depends!”

    It depends on your applications security requirements. I would not recommend it for an application dealing with private data (i.e. medical records or email) or lots of money (think e-banking). But for applications with less strict security requirements: the benefits might outweigh the risks.

    But I agree: It just does not feel right :-)

    Pascal

    P.s. Don’t tell anyone that I have a Post-It with the root password under my keyboard in my unlocked office.

  16. simplificator » Blog Archive » Rails 2.0 and Session Cookie Store said,

    December 18, 2007 @ 2:51 pm

    […] A long article with some mathematical background (it is insecure because of a flawed random generator) […]

  17. Sligo said,

    December 21, 2007 @ 5:41 am

    I am starting to warm up to the idea. I guess the important thing to remember is that you definitely have a choice, and for folks to get bent out of shape over the DEFAULT session mechanism is a bit extreme. We still have lots of options, and if people didn’t shake up things once in a while, we would never have progress…

  18. rtg said,

    July 10, 2008 @ 11:01 am

    Is it right that the crazy side effect on this is that the default app issues the same cookie id to all anonymous users?
    I took LovedByLess switched to cookie store and issues exactly the same session id to all users on the same machine…
    thx rtg

  19. 基于CookieStore的session存储机制的安全话题 - IceskYsl@1sters! said,

    January 3, 2012 @ 5:14 pm

    […] 最近在看一本《The Rails way》的书,其中关于session存贮机制一章中,对基于CookieStore的session存储机制持否定态度,认为其存在被破解和Replay attack的可能。就我了解的信息来看,这个观点是不妥的。 最详细的一篇文章是写的laigongli写的这篇“Rails 2.0, cookie session store and security”,文章比较长,说的很详细,感兴趣的可以过去看看。其中主要观点为: […]

  20. Rails宝典八十四式:Cookie Based Session Store | 南龙的小站 said,

    July 6, 2012 @ 4:31 am

    […] based session store的安全问题大家不用过分担心,详情请看:http://izumi.plan99.net/blog/index.php/2007/11/25/rails-20-cookie-session-store-and-security/ 未分类不用, 存储, 安全问题, 宝典, 很好, 担心, 符合, 请看, 迁移, 过分 […]

RSS feed for comments on this post · TrackBack URI

Leave a Comment