The question of the security of cookie-based session storage in Rails has pretty much been settled it seems to me. Out of the box, Rails uses cookie-based session storage. When you generate a new Rails app you get a nice new 128 character long (numbers and lowercase letters) secret set in config.action_controller.session[:secret] in your Rails::Initializer. That secret is used to sign and validate cookies for your application. Now the cookie data isn’t secret mind you, but it is tamper-proof. Good.
Now what if you don’t use cookie-based session storage at all? Well, just because you aren’t using cookie-based session storage doesn’t mean you aren’t using cookies. If your application has sessions at all, be they memcached ones or ActiveRecord ones, it is probably using cookies. It’s using cookies to store the session id so that when a request arrives, that id can be mapped to the corresponding session storage.
“So what” you say. “Well” I say… isn’t it cool that Rails generates that big random secret for you when you use cookie-based session storage? When we are not using cookie-based session storage, and that secret is not generated, don’t you wonder what secret is being used to secure your session id’s? You see, a session id must be hard to guess lest bad people gain access to your site. Usually when you want to make something hard to guess, you start with a secret and mix that with something that changes a lot and hash the whole shebang. So I went in search of this other secret.
What I found was that Rails calls CGI::Session#create_new_id to generate new session id’s. That routine uses no secrete per se. It hashes (MD5) a combination of:
- the current date and time (expressed as a human-readable string)
- the microseconds elapsed since the last second (expressed as a human-readable string)
- a pseudo-random number greater than zero and less than one (from Kernel#rand)
- the current process id number
- the string ‘foobar’
Notice there is no secret keying material there. “But what about the Kernel#rand call Bill!” I hear you saying. If you go have a look at Kernel#rand and Kernel#srand you’ll see that if rand is called before srand is called with a number parameter then the random number will be generated from a combination of:
- the current time
- the process id number
So the security of these session ids hinges on the secrecy of current time (on the server running Ruby) and the process id. Given that the system time is returned in HTTP headers and process id’s are often in the hundreds or thousands, it’s only really the microseconds that are hard to guess here, from a statistical standpoint. Others have expressed similar concerns.
If you’re worried about this two suggestions come to mind:
- time out sessions on the server so that an attacker has to guess faster
- monkey-patch CGI::Session#create_new_id to hash its result with a great big old 128 character secret
Updated: October 15, 2008 expanded analysis of Kernel#rand and Kernel#srand and updated suggestions.