A server that accepts a pre-supplied session token and elevates it on login lets an attacker fix the ID before authentication and collect the authenticated session after.
Session fixation lets an attacker decide the token before the victim ever logs in. If the server promotes a pre-supplied identifier into an authenticated session rather than issuing a fresh one at login, the attacker who supplied the ID now holds a valid authenticated session - without knowing the victim's password and without intercepting any response.
The attack unfolds in sequence. First, the attacker obtains a valid unauthenticated session token from the target - by visiting the login page, for example. Second, the attacker delivers that token to the victim's browser: through a URL parameter the application copies into a cookie, through a sub-domain cookie-set if the same-site boundary is loose, or through a meta-refresh redirect that carries the token in the query string. Third, the victim authenticates normally. If the server does not rotate the session identifier at that moment, the attacker's pre-placed token is now the victim's authenticated session - collectable immediately.
The confirming probe is a two-step differential. In the first request, issue a login with a controlled test account while presenting a pre-chosen session identifier in the Cookie header. If the response does not set a new session cookie - or sets one with the same value - the server did not rotate. In the second request, send a state-revealing request (account profile, dashboard) with the original pre-chosen token from a separate, cookie-clean session. If the server returns the authenticated profile for the test account, the boundary is confirmed open. The test account is controlled by the probe; no real user session is touched and no credential is harvested.
# Step 1 - login with a pre-chosen token and controlled credentials POST /login HTTP/1.1 Cookie: session=apNonce62615533 Content-Type: application/x-www-form-urlencoded username=probe-user&password=probe-pass HTTP/1.1 302 Found Location: /dashboard # No Set-Cookie - server kept the pre-supplied token. Fixation confirmed. # Step 2 - use the same token from a clean session (no cookies sent before) GET /dashboard HTTP/1.1 Cookie: session=apNonce62615533 HTTP/1.1 200 OK X-User: probe-user <-- authenticated, token never rotated
A rotation-aware server returns a Set-Cookie with a fresh, unpredictable value in the login response. The absence of that header - or a header echoing the pre-supplied value - is the confirming artifact. No brute-force, no real victim interaction, and no interception of any response the server generated for a real user.
The mechanism that plants the pre-chosen token in the victim's browser changes the delivery step but not the confirming probe. Some vectors require a specific server configuration or a loose cookie-setting boundary; others exploit design patterns that look intentional but were never meant as security controls.
| Delivery vector | Precondition | Confirming probe adjustment |
|---|---|---|
| URL-to-cookie copy | Server reads ?sessionid= from the URL and sets it as the cookie value | Present token in the URL parameter; confirm the Set-Cookie value matches |
| Cross-subdomain cookie set | Attacker controls a subdomain sharing the same domain cookie scope | Set cookie from the sibling subdomain; confirm it is visible on the login subdomain |
| HTTP response splitting | CRLF injection in a response header allows injecting Set-Cookie | Inject Set-Cookie via CRLF; confirm token accepted at login |
| Pre-auth session not rotated | Server issues a token before login and never replaces it | Present pre-auth token in Cookie at login; confirm absence of new Set-Cookie |
The most common case in modern applications is the last row - a framework that issues a pre-authentication session for CSRF or cart state and never regenerates it on successful login. No cross-subdomain or CRLF precondition is needed; the confirming probe is identical to the one above.
Token non-rotation alone is the root cause and always warrants a fix. Severity in the risk score is set by whether an attacker can realistically plant the token in the victim's browser. A non-rotation finding on an application with no URL-copy vector and no subdomain the attacker can reach is rated medium - real but harder to exploit. A non-rotation finding where the application copies a URL-supplied session ID into a cookie, accessible over the public internet, is rated high - the attacker only needs to send the victim a crafted link. A critical rating applies when CRLF injection or another direct injection path can set the cookie in the victim's browser without any click, combining two confirmed findings into a single chain.
The fix is always server-side and always the same: call the session regeneration function immediately after a successful authentication event, before issuing any redirect. The new token must be generated from a cryptographically secure source - not derived from the old one and not reused from the URL parameter. Most frameworks expose a one-call fix: session.regenerate() in Express, request.session.cycle() in Rack, SessionFixationProtectionMixin in Django. Not calling it is the entire bug.
Every finding carries the two-step PoC - the controlled login request and the clean-session retrieval - so the fix is verifiable by the engineer who implements it: run the same two requests, confirm the second returns a 401 or 403, and the session boundary is closed.