Back to Feed
article 3m read

Hashed session tokens, then a fallback let the hash itself log in

The move to hashed session storage kept a fallback that also matched the raw cookie value, so any row from a leaked table would have worked as a login until I pulled that branch out of every copy.

Article DepthPractice

Flow

Three ways to handle old sessions

  1. 1Sign everyone out

    Simplest. Each user signs in once, and the table holds only hashes from then on.

  2. 2Mark the new rows

    Keeps old sessions alive, but needs a version tag or a second column and an end date.

  3. 3Match both ways

    What I shipped. A stolen hash works as a login, so hashing protects nothing.

To keep a leaked database from giving anyone a login, I hashed my session tokens. Then I shipped a lookup that took the hash as a login.

My login service gives the browser a long random token when you sign in. It used to store that token as is. Whoever shows the token is the user, so anyone who could read the table could copy a row into a cookie and be that user. So new sessions now store a SHA-256 hash of the token. The browser keeps the real token. Each request hashes the cookie and looks for the hash. A fast hash is fine here, since the token is random and there's nothing to guess.

The fallback

I didn't want to sign everyone out on deploy day, and the commit message says so. So the lookup matched two ways. It looked for the hash of the cookie, or for the cookie exactly as sent, in case the row was an old one.

WHERE stored = :hash_of_cookie OR stored = :cookie

That second half lets in any string that is already in the table. And the table is what a thief has. Send a stored hash as the cookie. The first half hashes it again and misses. The second half finds the row, and you're logged in as that user, roles and all. It works every time. As long as that branch was there, the hashed table was no safer than the old one.

I couldn't limit the fallback to old rows, either. Nothing in the code marked a row as old or new. To keep old sessions, I'd have needed a marker on new rows, like a version tag or a second column.

Finding it and pulling it out

Nobody broke in that I know of. A second batch of findings went on my security to-do list right after, rated high. It described this trick. It also said the same two-way lookup had been copied into password reset, email checks and the MFA step.

The session lookup now only looks for the hash. That went into two copies, since the customer app and the admin app each have their own. The reset, email and MFA lookups lost their raw branch too.

From the code, anyone with a session from before the hashing gets sent back to sign in once. That costs them a password and a few seconds.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward