Web Security Foundations
Log in Join

Lesson 4 · Reading · 25 min

Sessions and identity

In the first lesson you learned that HTTP has no memory: every request arrives on its own, and the server has no idea whether this request came from the same person as the last one. And yet you stay logged in. This lesson is about how a server remembers who you are between two requests, and about the mistake that turns "remembering you" into "believing anything you claim."

The problem: a server with amnesia

You send POST /login with your username and password. The server checks them and… now what? The next request you send, GET /account, is a brand new connection as far as HTTP is concerned. Nothing about it says "this is the person who just logged in." If the server did nothing, you would have to send your password with every single request.

So after a successful login the server hands you a token of some kind, and your browser sends it back on every later request. That token is what "being logged in" actually is. It is not a state the server holds about you; it is a value you carry and present, like a wristband at a festival. The server sees the wristband and says "ah, you're in."

You met the mechanism already: the cookie.

Set-Cookie: session=8f3a9c1e77b2; Path=/; HttpOnly; Secure; SameSite=Lax

is the server giving you the wristband, and

Cookie: session=8f3a9c1e77b2

is you showing it on the next request. Whoever sends that value is treated as that user. That is the whole of session security, and where it goes wrong.

Two kinds of token

There are two honest ways for a server to remember you, and one dishonest shortcut.

The opaque session id. The token is a long random value that means nothing on its own, like 8f3a9c1e77b2 above. The server keeps a table: this random string maps to "user 4012, logged in at 9:14." When the request comes back, the server looks the string up in its table. You cannot forge a valid one because you cannot guess a random value that is already in the server's table, and you cannot make the table say something it does not. This is the classic, and still the most robust, approach. The token is a claim check; the real data lives on the server.

The signed, self-contained token. Sometimes the server does not want to keep a table, so it puts the data in the token and signs it: "user=4012; role=admin" plus a cryptographic signature only the server can produce. When it comes back, the server re-computes the signature and checks it matches. If you change "role=admin" to something else, the signature no longer matches, and the server rejects it. A JWT (JSON Web Token) is the common form of this. The data is visible to you, but you cannot change it without breaking the signature. The safety is entirely in the signature, and a whole class of bugs, which the Academy covers later, comes from signatures that are checked badly or not at all.

The dishonest shortcut, and the bug this lesson shows. The server puts your identity in the token and does not protect it: no random lookup, no signature. A cookie like user=guest or role=user, or a base64 blob that decodes to {"user":"guest","role":"user"}. Base64 is not a lock; it is just a way to write bytes as text, and anyone can decode it, change it, and encode it back. If the server reads your role straight out of a value you sent and trusts it, then you decide your own role. Change user to admin, send it back, and the server believes you, because you are the only thing it was ever checking.

Why base64 fools people

It is worth dwelling on this, because it catches beginners and developers alike. A cookie value like eyJ1c2VyIjoiZ3Vlc3QifQ looks encrypted. It is not. Those characters are base64, an encoding that turns arbitrary bytes into letters and digits so they survive being put in a header. Decoding it is one function call (atob() in the browser console, base64 -d in a terminal), and it reveals the plain text inside. Encoding your own version back is just as easy. Encoding is not encryption and it is not signing: it hides nothing and proves nothing. When you see a chunk of base64 in a cookie or a token, your first move is always to decode it and see what the server is actually trusting.

What good looks like

You do not need to build secure sessions, but recognising them helps you know when you have found a bug and when you have not:

  • The token is opaque (random, meaningless without the server's table) or signed (self-contained but tamper-evident). If you can decode a token, change your identity in it, and the server accepts it, that is a finding. If you change it and the server rejects it, the signature is doing its job.
  • The cookie carries protective attributes: HttpOnly (JavaScript cannot read it, which limits what an XSS bug can steal), Secure (only sent over HTTPS), SameSite (not sent on requests from other sites, which blunts a class of cross-site attacks). Each missing attribute is its own smaller issue, and the Academy has shelves on them.

The single idea to carry out of here is the same one from the last lesson, pointed at identity: your identity is a value you send, and if the server trusts it without proof, it is yours to change.

Now do the lab

The lab is a site called Ledger. It logs you in as a guest and greets you by name. There is an admin area you cannot see. But the thing that decides you are a guest and not an admin is a cookie the server handed you, in plain base64, with no signature. Decode it, change who you are, send it back. The admin area, and your flag, are waiting for whoever the server thinks you are.

This lesson has 1 lab to try what you just read.