Jeremy Kemper has just committed a new session storage for rails, and it's going to be a doozy: it's in the cookies.

Pushing the storage out to the client-side is an interesting decision, and one fraught with security concerns, because you can no longer guarantee that the data has not been tampered with. However, a look at the implementation reveals that he has, indeed, covered his bases.

If you're interested, the commit is here and Ryan covered it here briefly.

why

For most rails apps, the makeup of data stored in the session is something like

  • a flash message (flash[:notice])
  • the user_id
  • some user preferences

In this case, the cookie storage is ideal. However, if you're storing marshalled ruby in the session (for example,

 session[:user] = User.find( ... )

then, remove that code! It's slow and bad practise! Did you know you have to marshal and un-marshal the ruby object from a string for every request?

how it works

The cookie has a number of values, as does a normal session. The cookie store takes the contents of the cookie, hashes them with SHA512 with your app's secret key, and stores the result of that hash in the cookie along with the original data.

This means that tampering with the values from the client side is impossible, unless you know the application's secret key, or unless you can hack it.

security

If you can hack SHA512, then you can hack the session storage. I'm no security expert, but I believe this is a fairly difficult thing to do. In ideal circumstances (for the cracker) you'd need to be able to change one variable reliably, and then generate a large number of sessions. And then hack SHA512.

You can actually change the method of hashing the cookie, in order to make it more secure. Simply override this method with your preferred signing and/or encryption method.

class CGI::Session::CookieStore
  def generate_digest(data)
    Digest::SHA512.hexdigest "#{data}#{@secret}"
  end
end

This way you can actually make it much more difficult to hack the cookies, since (a) your closed-source web app doesn't conform to the normal 'rails' way of doing things, and (b) you can include other factors into the key.

Other ideas for security:

  • Since the session auto-expires if the hash changes, you can have all kinds of fun. You could add the subversion revision number to the secret and thereby automatically expiring all sessions each time you deploy.
  • Load a file from the server and add that into the secret key.
  • Add another "secret" key just to be sure
  • Hash the SHA with a different key
  • Add the remote IP address and maybe even user-agent to the data to help prevent session hijacking.
  • Do some fancy public-key or other asymmetric encryption and store the private key in their user object

The possibilities are endless, but for most people, no changes should be necessary.

The other way of hacking it is, find out who the developer of the site is, and steal his laptop. Hopefully the key will be the same between the production server and the development box (this is where 'load a file from the server' comes in handy). Then you'll be able to change user IDs at will and pwn the site.

Other issues

  • Session expiry. Dan Peterson had the concern that the application can no longer control cookie expiry, but as the expiry is stored in the cookie, it's impossible to change without hashing.
  • Bandwidth. A cookie can contain up to 4K of data, so that's potentially a packet or so you're adding to every request. Caveat emptor.
  • No cookies. If a user doesn't have cookies, then they're not going to get rails sessions anyway. Everyone has cookies now, don't they? Feel free to prove me wrong.

8 Responses to “new controversial default rails session storage: cookies”

  1. Jamie Says:

    Re: server-controlled expirations, I want to suggest adding something like “the current day” to the secret, except robust enough to not screw people who log in just before midnight.

  2. Peter Cooper Says:

    Next step.. storing all that data in a big ugly param on the URL PHP-sty-lee when the user doesn’t have cookies :)

    From the code I read, it’s just a Base64 encoded version of the Marshal dump. I have had great success gzipping after a Marshal dump and /then/ Base64 encoding it. I store rarely used feed metadata in this way with typical 60% space savings in my database. Of course, it tends to only result in major savings if the Marshalled data is over about 200 bytes in length.

  3. Peter Cooper Says:

    I meant zlib rather, but it’s almost all the same ;-)

  4. Will Green Says:

    At work, we develop in ASP.Net. This technique sounds very similar to ViewState, only ASP.net store the data in a hidden field in every page. This work because EVERY ASP.Net page is a form, and even many hyperlinks (specifically LinkButtons) submit this form (via JavaScript… yech).

  5. RailsNoob Says:

    Why not include salt by default to protect the secret key from chosen-payload and dictionary attacks?

  6. Eric Hodel Says:

    Marshal::load is a lot faster than ActiveRecord::Base#find, about 40x for my code:

                       user     system      total        real
    empty          0.010000   0.000000   0.010000 (  0.008378)
    Marshal.load   1.760000   0.000000   1.760000 (  1.764902)
    User.find     43.040000   3.210000  46.250000 ( 68.689680)

    I can email you my benchmark script, if you’d like. (You also don’t need to Marshal or fetch the user from the DB every request, you can retrieve it only as needed.)

    By the way, my user looks like this:

    --- !ruby/object:User 
    attributes: 
      username: drbrain
      id: "2"
      builds: "2311"
      password: password

    I don’t think this is very secure, either. Apps storing data in the cookies are open to information disclosure. Third parties can read sensitive cookie data on the wire, which should be minimized as much as possible.

    The app’s secret is also open to a brute-force attack. With a simple ruby script I can generate 100,000 SHA512 digests in 1.6s on my machine.

    Instead the cookie data should be encrypted, not verified with a digest. This prevents brute-force attacks on the plaintext and prevents information disclosure.

    You probably don’t want to expire sessions every time you push an app. This gives poor user experience since users will have to do things like log-in again with every change to the website.

  7. furburgher Says:

    Yeah, but viewstate is a big blob of dog crap.

  8. Ned Baldessin Says:

    Working with disk based sessions, you could assume that by default, the attacker could not read the data you stored in the session. It was a black box. This is no longer the case, so you have to keep that in mind.

    Has anyone benchmarked the performance gain you get from this?

Sorry, comments are closed for this article.