Backend Systems · Session and State

Session Fixation, Rotation, and Revocation

Why authenticating in place hands an attacker a valid session, how rotation must be sequenced to survive concurrent requests, and what revocation can and cannot reach.

Authentication changes what a session is worth. Before it, the record holds an anonymous visitor and a CSRF token; after it, the same record holds a user identity with whatever authority that implies. If the record’s key does not change at that moment, then anyone who knew the key before authentication knows it after — and knows it is now authenticated.

That is session fixation, and it is entirely a failure to rotate. The attack needs only a way to make the victim’s browser carry an identifier chosen by the attacker: a link with the identifier embedded, a subdomain that sets a cookie for the parent domain, an injected script, or a framework that accepts an identifier from a request parameter. None of those are exotic, and the defense is a single write that discards the old key.

The upgrade-in-place mistake

A naive login handler finds the existing session record, sets the user field, and continues. It reads as an efficiency: no new record, no new cookie, the cart and the CSRF token survive. It also means the identifier that was issued to an unauthenticated visitor, and that may have been visible in a URL or planted by a third party, is now the identifier of an authenticated user.

The correct sequence invalidates first and issues second: create a new record, copy forward the fields that must survive, delete the old record, and send the new identifier in a Set-Cookie on the login response. Copying before deleting matters — the reverse order leaves a window in which the user has neither session. Deleting the old record matters as much as creating the new one; leaving it as an anonymous session means the planted key still resolves, just to less authority, and any code path that treats a record’s presence as evidence of prior interaction now has a lie in its store.

Frameworks expose this under names like session regeneration or session ID renewal. What matters is not the name but that the old key is removed from the store, that the new cookie is written with the full attribute set rather than a subset, and that any cached copy of the record is invalidated. Cookie Scope, Attributes, and Transport Rules describes what a partial attribute set costs.

Every boundary, not just login

Login is the loudest privilege transition, not the only one. An elevation to an administrative role, a switch into another tenant or account context, an impersonation feature, a password change, and a re-authentication prompt after a period of inactivity all change what the identifier is worth. NIST SP 800-63B treats reauthentication as a distinct event with its own freshness requirement precisely because the authority attached to a session is supposed to decay.

A password change deserves special mention because it is both a privilege event and a revocation event. Rotating the current session protects the user who made the change; revoking all the user’s other sessions is what protects them from whoever had the old password. Doing the second without the first leaves the attacker holding a session that survived the credential reset, which is the outcome the reset was supposed to prevent.

Tenant switching is the case most often missed in multi-tenant systems. If the session record holds a tenant identifier that authorization checks read, then switching tenants is a scope change, and a token issued under the previous scope should not remain valid for the new one. The same reasoning applies to any value the record caches that authorization depends on.

Rotation under concurrency

Rotation is easy to describe and awkward to sequence when a browser is issuing several requests at once. A page load fires a document request and a dozen subresource requests within milliseconds. If the login response and a background request carrying the old cookie overlap, the server may see the old identifier after the new one has been issued.

Two failures follow. If the old record is deleted immediately, in-flight requests that carry it are treated as unauthenticated and may redirect to a login page the user has just completed. If the old record is kept as an alias pointing at the new one, an attacker holding the old identifier retains access for the alias’s lifetime, which reintroduces a smaller version of fixation.

The usual resolution is a short grace window with a redirect: for a few seconds after rotation, the old identifier resolves to a record that carries only a “moved” marker, and the server responds with a fresh Set-Cookie and a redirect rather than serving the request. The alias holds no authority, so an attacker who presents it gets a redirect and nothing else. The window must be long enough for requests already in flight and short enough that it cannot be relied on, which in practice means seconds rather than minutes.

A second hazard is response ordering. Two responses both containing Set-Cookie can arrive out of order, and the browser applies the last one to arrive, not the last one sent. If the older response’s cookie overwrites the newer one, the client ends up holding a key the server has already retired. Making the cookie value monotonically increasing — a generation counter inside the payload — lets the client-side code, or the server on next request, detect and discard the stale one. Where cookies alone are used, the practical mitigation is to rotate once per login rather than on every request, and to avoid issuing Set-Cookie from endpoints that can respond concurrently with the login.

Sessions that rotate on every request, a pattern borrowed from single-use token designs, multiply this race by the request rate. Rotation on privilege change has a bounded, human-scale frequency; per-request rotation requires a client that serializes its own requests, which browsers do not do.

What revocation can reach

Revoking one session is a delete by key. Revoking all sessions for a user requires an index from user to session keys, because the store is keyed by identifier and has no secondary index. Maintaining a set per user is the standard approach: on login, add the key to user:{id}:sessions; on logout, remove it; on “sign out everywhere”, iterate the set and delete each key, then delete the set. The set itself needs an expiry and a bound, or a user who logs in from many devices accumulates an ever-growing list of dead keys.

Iterating a set of unknown size inside a request is a latency hazard. A user with a thousand entries, each a round trip in the worst case, turns a logout into a slow operation that can time out halfway and leave some sessions alive. Batched deletes and a cap on concurrent sessions per user both help; capping also removes the unbounded-growth problem and is a policy most users never notice.

The alternative to enumeration is a version floor. The user record carries an integer that is bumped on “sign out everywhere”, and each session record stores the version it was created under. A request whose session version is below the current floor is rejected and deleted. This makes mass revocation a single write, at the cost of a read of the user record on each request or a cached copy that is trusted for some interval. Caching the floor for even thirty seconds means up to thirty seconds in which a revoked session still works, which is a stated staleness budget rather than a bug. Signed Cookie Tokens and the Limits of Statelessness applies the same construction to tokens the server cannot delete at all.

Replication makes revocation eventually consistent

Deleting a key is a write to the primary. A read served by a replica may still return the record for as long as replication lag lasts, and during that interval the revoked session is accepted. The window is normally milliseconds, but it is unbounded in principle: a replica that falls behind during a network partition can serve a deleted session for as long as the partition lasts.

For logout and password-change flows, the read that follows the write must go to the primary. That is a routing decision in the session lookup path, and it needs to be explicit — a connection pool with separate primary and replica handles, and a rule that session reads are primary reads for a bounded period after any write to that session. Treating revocation as strongly consistent when the store is not is how “signed out on one device, still active on another” reports arise.

A logout that is reachable by GET can be triggered by any image tag or prefetched by a browser, so a third-party page can log a user out at will. The effect is limited — denial of service against one user — but it also means a GET logout can fire from a link scanner in an email, invalidating the session before the user clicks. RFC 9110’s safe-method distinction exists for exactly this: state-changing operations belong to POST, with a CSRF token, and the response should not be cacheable.

Server-side, logout must delete the record and expire the cookie with a matching name, path, and domain. A cookie expired with the wrong path leaves the original in place, and the browser keeps sending it; the session appears to survive logout even though the store entry is gone, and the next request re-creates an anonymous session under the same key, which looks like a session that refuses to die. Key Design and Eviction in External Session Stores covers the composite-key semantics of cookie replacement in the context of store writes.

The residual surface

Rotation and revocation cover the server’s view of a session. They do not cover a copy of the token that was taken before rotation — rotation invalidates the old key, which is precisely the point, but only for keys the server knows about. A token captured from a request after rotation is as good as the session it names until the next rotation event. Bounding that requires either short validity with frequent re-issuance or anomaly detection on the session’s use, and detection is a probabilistic control rather than a guarantee.

References

  1. OWASP Session Management Cheat Sheet — OWASP Foundation
  2. OWASP Session Fixation — OWASP Foundation
  3. NIST SP 800-63B: Digital Identity Guidelines, Authentication and Lifecycle Management — NIST