How to almost protect yourself from the Rails cookie session store

By Najaf Ali

Much has been said about the insecurity of storing Rails sessions in cookies. It leaves you open to:

  • Session replay - users can replay their session at any given state and your application would have to accept it.
  • No meaningful logout - you have no way to invalidate sessions as they're delivered by the user on every request.
  • Readable sessions - if an attacker can grab your secret key base, they can read all of their session data.
  • Writable sessions - again with your secret key base, an attacker can write arbitrary values into their session that your application would have to accept. In most Rails applications this probably means they can authenticate as any user they like.

Most discussions of the security drawbacks of the cookie session store stop there. This doesn't come close to the biggest risk cookie-based sessions (the way Rails implements them by default) pose for your application.

The alpha-dragon mack-daddy holy grail of security vulnerabilities is one that allows an attacker to perform remote code execution. If an attacker can grab your secret key base, they can do just that. They can run arbitrary code in your Rails application process at will. That includes shelling out.

With feeling:

If you use the cookie session store in Rails then your value for secret_key_base is as good as an ssh private key for box your application runs on.

I've failed to convey the gravity of this vulnerability in conference talks, presentations given to development teams, blog posts, tweets and emails. The only way I've convinced developers to take this one seriously is to make them implement the exploit themselves in the Rails security workshop I run.

But I won't ever lose my secret key base!

There are at least three ways you could lose your secret key base to an attacker that I can think of:

  • If you "open source" your web application and publish the secret key base on GitHub.
  • (If you realize your mistake and then commit a delete without changing it in production, still counts!)
  • If you implement a broken file upload/download mechanism that allows users to download arbitrary files.
  • If you package your Rails apps as appliances and use the same secret key base for all customer instances.

Experienced security researchers can probably think of plenty more.

The sensible choice: a server-side session store

Putting your sessions in a server-side store (while only giving a token consisting of random data to the user as a key into it) mitigates all of the security problems above. Pick Redis. Redis is so useful that you probably have it lying around doing something for you already. Stick your sessions and redis and avoid all of this drama.

But you're not going to do that. Almost no one I advise to put their sessions on the server does it. They grumble about a performance overhead, limits on RAM or not being able to scale horizontally. I'm not convinced by these objections. You might be, so let's move swiftly on.

JSON cookie serialization

Finally some good news! It turns out that as of 4.1.0, in newly generated apps cookie-based sessions are serialized using JSON instead of Marshall. Better yet, it looks like you can't use the JSON serializer for arbitrary objects, only "primitives" i.e. strings, numbers, booleans, arrays and hashes.

This doesn't mitigate the other security issues above, but it does save you from the remote code execution vulnerability.

You don't get this simply by upgrading.

Try generating a new Rails application at a version after 4.1.0. You'll notice an initializer named cookies_serializer.rb. It's contents will be something like:

<%= code 'cookies_serializer.rb' %>

Adding an initializer with that config value setting will make Rails use JSON to serialize your session data instead of the default Marshal.

This will invalidate all existing sessions. If you don't care about this, then you can skip the rest of this post. If you'd rather your users aren't all booted out of the application when you deploy this change, Rails has you covered.

Instead of :json, set the value to :hybrid. This will cause Rails to accept sessions serialized with Marshal and exchange them for sessions serialized with JSON.

After you're confident that all your users sessions have been converted to JSON, you can roll out another release that flips the config value to :json.

Note: If you're storing complex Ruby objects in the session and need them to be serialized with Marshal, you won't be able to use the JSON serializer.

You should still seriously consider keeping your sessions server side, but switching to JSON session serialization at least prevents you from being owned comprehensively.