~/learn / authorization / sessions-and-cookies

Sessions and Cookies

How websites remember you are logged in, how the cookie jar and its attributes actually behave, the backends that store session state, and the fixation, prediction, and invalidation mistakes that let sessions be stolen or reused.

beginnerupdated

HTTP is stateless. Every request arrives on its own with no memory of the last, so a server needs a mechanism to recognize that the request in front of it comes from someone who logged in five minutes ago. That mechanism is the session, and the cookie is how the browser carries the session’s name card on every request.

The classic session flow

  1. You submit a username and password.
  2. The server verifies them, generates a random session id, and stores the mapping from that id to your user and session state on the server side.
  3. The server returns the id in a cookie: Set-Cookie: session=7f3a...; Secure; HttpOnly; SameSite=Lax.
  4. Your browser automatically attaches Cookie: session=7f3a... to every later request to that site, with no code required.
  5. The server looks up the id and knows who you are.

The session id is effectively a bearer token and a temporary password rolled into one. Anyone who holds it is you until it expires or is revoked, which is why every attribute below exists to keep it from leaking.

Cookies are matched to requests by a specific algorithm, and the surprises live in the details:

  • Scope is domain and path. A cookie for example.test is sent to example.test and, if Domain is set, to its subdomains. This is why a single compromised or forgotten subdomain can read or clobber cookies for the whole domain. Cookies ignore ports and, for the most part, scheme, so “same site” is broader than “same origin.”
  • Attributes are not sent back. The browser stores Secure, HttpOnly, SameSite, and the rest, but only the name and value are returned in the Cookie header. The server cannot see from the request which flags were set, so it cannot rely on reading them back.
  • Size and count limits are real (roughly 4 KB per cookie). Overflowing them silently drops cookies, which has caused authentication to fail in confusing ways.

The attributes that matter

Attribute What it does Without it
Secure Only sent over HTTPS The cookie can leak over plain HTTP
HttpOnly JavaScript cannot read it An XSS bug steals the session directly
SameSite=Lax or Strict Withheld on most cross-site requests CSRF becomes far easier
Path, Domain Limits where it is sent A broad Domain leaks it to every subdomain, including forgotten ones
Max-Age or Expires When it stops being stored A pure session cookie can outlive its welcome if the browser is never fully closed

The __Host- prefix is an underused gem worth knowing. A cookie named __Host-session is only accepted by the browser if it is Secure, has Path=/, and has no Domain. Browsers enforce those conditions, so a compromised sibling subdomain physically cannot set or overwrite a __Host- cookie, which shuts down an entire class of subdomain cookie-injection attacks. Inspect any site’s cookie flags with the header analyzer.

Where the state lives

The session id is only half the design. The other half is where the server keeps the state the id points to.

  • In-memory on one server is simple but breaks the moment you run more than one instance, unless requests are pinned (sticky sessions), which is fragile.
  • A shared store (Redis, Memcached, a database) lets any instance resolve any session and, crucially, lets you delete a session to kill it instantly everywhere. This is the usual production choice.
  • Self-contained tokens (JWTs) keep the state in the token and store nothing server-side. That scales trivially but makes immediate logout hard, because the token is valid until it expires unless you maintain a denylist, which reintroduces the server-side state you were trying to avoid. The trade-off is precisely how fast you can revoke a stolen credential. See JWT.

What goes wrong

  • Predictable ids. Session ids must come from a CSPRNG with enough length (128 bits or more). Sequential, timestamp-based, or md5(user_id) ids can be guessed or computed, letting an attacker ride another user’s session without stealing anything.
  • Session fixation. If the server keeps the same id before and after login, an attacker plants a known id in the victim’s browser (through a link, a subdomain, or a cookie-setting bug), waits for the victim to log in, and then uses that same id, now authenticated. The fix is a single line of discipline: issue a brand-new session id at the moment of login (and of privilege change), discarding the pre-login one.
  • No server-side invalidation. Logout that only clears the browser cookie leaves the session record alive; replaying the old cookie still works. Logout must delete the server-side session.
  • Immortal sessions. No idle timeout and no absolute lifetime means a stolen cookie works for months. Enforce both an idle timeout and a maximum age.
  • Password change does not revoke. Changing a password, or detecting compromise, should terminate every other session for that account. If it does not, the attacker who prompted the password change keeps their session.

Pull the Set-Cookie header and read every attribute, because each missing one is a finding:

  • No HttpOnly means XSS can read the session with document.cookie. No Secure means it leaks over any plaintext request. No SameSite reopens CSRF.
  • Scope too wide: Domain=.example.com shares the cookie with every subdomain, so a takeover or XSS on blog.example.com steals the main app’s session. The __Host- prefix forbids a Domain attribute and forces Secure and Path=/, which is the strongest binding.
  • Session not rotated on login. Note the session id before authenticating, log in, and check it changed. If it did not, the app is vulnerable to fixation: plant a known id in the victim’s browser (via a Set-Cookie from a subdomain, a URL parameter, or XSS), wait for them to log in, and reuse that id.
  • Session not killed on logout or password change. Capture a session, log out in another window, and replay the captured request. If it still works, revocation is broken, which is how stolen tokens stay useful.

Field notes: predictability and token handling

Collect a handful of session ids and look at them. Sequential, timestamp-based, or short tokens are guessable or brute-forceable; Burp Sequencer quantifies the entropy. If sessions are JWTs in a cookie rather than opaque ids, the whole JWT attack surface applies, and server-side logout is often impossible because the token is self-contained (check whether a “logged out” JWT still works until expiry). Cookie tossing is the subdomain-trust attack in reverse: a weaker subdomain sets a cookie that shadows the parent’s and forces your value onto the victim. Only test sessions you own or are authorized to assess.

Testing sessions in a lab

With a proxy capturing traffic on an application you are allowed to test:

  1. Log in, note the cookie, log out, then replay a request with the old cookie. Does it still work? (invalidation)
  2. Compare the session id before and after login. Did it change? (fixation)
  3. Log in from two browsers, change the password in one, and check whether the other stays logged in. (revocation)
  4. Collect many fresh session ids and look for structure or sequence. (predictability)
  5. Inspect every Set-Cookie for the attributes above with the header analyzer.