You need to hash web app session IDs
August 8, 2011
Up until very recently (in fact, today), I could confidently recite what I thought was the best way to handle login sessions in web apps: every time someone logs in, you pick a large random number to be their session ID, give them a cookie with the session ID, and then store all of the details in your database under the session ID. When they come back, you get the session ID cookie, look it up in the database, make sure it's (still) valid, and go.
This approach is dangerously incomplete, as I discovered by reading a stackoverflow thread on web authentication (via Hacker News). To be secure you need to always store your session IDs in your database in some cryptographically hashed form, never in plain text.
To see why, suppose an attacker gets surreptitious read access to your database and you use plaintext session IDs. Of course you follow best practices and salt and hash your user passwords (using something like bcrypt), so that they are not feasibly recoverable. However, the attacker has all of the session IDs for all active sessions. While those sessions remain valid, the attacker can hijack any of them (by creating their own valid session cookie for the session) and thus do anything in your app as any currently logged in user that doesn't require people to reauthenticate. Depending on your application this may be quite a lot, especially if the attacker gets the sessions of administrative users.
(This is one reason why you should require the account's current password in your password change form, something that I hadn't quite realized until I started thinking about it now.)
On a side note, this applies just as much to sessions that use session cookies as it does for persistent cookies (contrary to what the stackoverflow writeup implies), because an attacker can use valid session cookies just as easily as they can use valid persistent cookies. What matters is whether they can create appropriate cookies given a session ID (you should assume yes) and whether that session is still valid, not what form the cookie takes and what options you've set in your official cookies.
As mentioned, to solve this you need to store the session ID only in some hashed form, just as if it was a password (because in fact it is a somewhat limited password). I can see a number of approaches to doing this.
The simplest change is to hash all session IDs with the same global value, which means that an attacker cannot directly recover a session ID given database access (you should assume that they recover your global value along with your database). This reduces the attacker back to the birthday paradox attack on session IDs, although they can do this offline and in bulk; you should thus pick a slow hash function, like bcrypt, and make sure that you are still using a suitably large and random session ID. The advantage is that you need no cookie or (significant) database changes; you simply hash the session ID before recording it in the database, and then hash it again before looking it up.
The better change is to treat session IDs like passwords and salt them individually. However, this means that you need some way to recover the session ID record given information in the cookie so that you can find the right salt. I can think of two approaches. First, you can store some index to the user record in the cookie, then have a way of recovering all sessions records for a given user; this gives you a feasibly small number of sessions to check (by taking each session's salt, hashing the salt plus the session ID from the cookie, and checking to see if it matches the session record's hash). The other approach is to directly store a key to the session record itself in the cookie. In effect, what you have done is change the problem; your hashed 'session ID' is in fact a session ID validator, and the key is the real session ID (which you might as well make a full sized random number of however many bits).
If I was doing this in a web app, my preference would be for the second approach because it leaks less information into the cookie. The first approach necessarily puts some sort of information about the user there, which might be useful for attackers or eavesdroppers.
(I'm sure that all of this is well known in the web app security community; I just feel like writing it down so that it sinks into my head. See the stackoverflow thread for more.)
PS: I maintain that you can't solve this by signing your session cookies with HMAC and a server-side secret; you should assume that your server-side secret can be compromised just as your database was. And if you are going to believe in a server-side secret, you might as well use the 'hashed with a global value' approach to storing session IDs with the server-side secret as the global value; you're just as well off either way if the attacker compromises only the database, and you're better off if the attacker compromises both the database and the server-side secret.
Written on 08 August 2011.
* * *