Rails in Session

4 minute read

Last time I took a reasonably deep dive in cookies. Cookies can keep state information and setting for visitors to a site. However, by default they aren’t secure, being sent in the clear and easily modified. You can turn on security option, but an even easier is stepping up to sessions.

“Sessions” are a common concept across web development platforms, but interestingly, they are actually a standard like, say, cookies. Sessions use cookies and typically work more or less the same what. However, each implementation is different, a Rails session would not be readable by PHP and vice versa.

The concept it self is simple. Instead of storing different values in separate cookies, have a single cookie that can be used to looking up a collection of settings for a “session”. Actually, that’s lie given how the default Rails (2+) implementation works, but let’s run with it as it’s easier to explain.

In Rails, session is a special hash like object available in your controller, not that different from the cookie hash-like thing. You can set values:

session[:current_user_id] = user.id

and retrieve them anytime the visitor returns:

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

The differences are that the session is represented by just one cookie, which is the sessions id and, as a result, the data stored in the session is not available to the user. They just see the session id.

Under the hood, you could image it looks something like:

session = Session.find(cookie[:session_id]).data

where data is a hash.

Now, if you were paying attention when I wrote about cookie security, you might see a problem. As I described, it’s relatively easy to modify a cookie. If I can guess someone’s session ID and swap out mine for there’s, boom I’m logged in as them. This is solved by making the session id secure in some way, the easiest method being to use a big random number, making it impossible, or at least very, very impractical to guess a value id.

So, that’s how they work, but what was my lie? That sessions are stored on the server.

If you store session data on the server, you have to deal with it. In very old (pre 2.0) versions of Rails, sessions were stored in individual files. If you forgot to clean them up, you’d find a grumpy sysadmin banging on you door wanting do know what these 50,000 files were filling her disk. You could use a database instead, but now you have 50,000 rows. Sessions are automatically created when you use them, but cleaning out old ones requires some sort of process. That’s unless you make it someone else’s problem.

The default Rails cookie store is EncryptedCookieStore. The session hash is serialized and set as the cookie’s value. An encrypted cookie is used so that the recipient can’t see the stored values. (Prior to Rails 4 CookieStore was used, which worked the same way, but wasn’t encrypted so stored values were (with a bit of effort) visible.

The cookie approach puts the burden of storing the session data on the visitor. The browser will clean up the cookie when it expires. Pretty handy! And, it solves the “guess the session id” issues, the session cookie value is a blob of encrypted text, effectively a big, random number.

There is a limitation however, the maximum size of a cookie is 4K, include all of the meta data, like the name of the cookie and the expiration and any encryption and serialization overhead. If you are storing more than 4K in your session, they you are probably doing it wrong. Typically, session data should be pretty small, a user id, a timezone, a language preference.

If you really want to store more you can look at the MemCache Store

Out of the box, Rails session cookies have HttpOnly, meaning the are not available to JavaScript and thus protected from cross site scripting attacks (where malicious JavaScript is injected to steal cookies). Do yourself a favor and leave it that way. (Cookies are sent by AJAX requests, so auth is unaffected.)

One issue that Rails sessions don’t solve out of the box is sending the session cookie securely. By default, they will be sent over HTTP, allowing them to be sniffed on the network. An attacker could then reused the cookie, hijacking the session.

Hijacking is easily prevented by using HTTPS and there’s no reason to run a web app without it, so set it up on your production server and set config.force_ssl = true config/environments/production.rb. As the name implies, this will force Rails to always use HTTPS.

In addition, edit config/initializers/session_store.rb and change

Rails.application.config.session_store :cookie_store, key: '_your-app_session'

to:

Rails.application.config.session_store :cookie_store, key: '_your-app_session',
                                                      secure: Rails.env.production?

This will tell the browser never to send the cookie over HTTPS. This prevents accidents if parts of your site outside of the Rails app are accessible via HTTP.

One last security tip, sessions stored in cookies are vulnerable to being replayed, so don’t store information you don’t want the user reusing. Say you had a game and your user can by credits to play it.

session[:credits] = 5

They buy five credits, save a copy of the cookie, and then proceed to play five games.

if session[:credits] > 0
   session[:credits] -= 1
   play_game
end

They are now out of credits. Now they set the cookie back to the original value they saved, back to five credits.

Design your sessions usage such that there is no advantage to reusing an old one:

user = User.find(session[:current_user_id])
user.credits = 5
if user.credits > 0
   user.update(credits: credits - 1)
   play_game
end

(Multiple logins and race conditions are left as a exercise for the reader.)

Now you have a little more depth with sessions, now we can finally talk about what happens when you don’t have cookies. But, that’s next time.

Comments