Session fixation is a simple trick with a serious result. The attacker gets a valid session identifier from your site, makes the victim use it, and waits for the victim to log in. Because the application never changed the identifier, the attacker now holds an authenticated session. CWE-384 defines it as authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier.
It hits any web application that uses server side sessions or session cookies, whatever the language. MITRE classes it as a compound weakness, meaning it appears when several conditions line up.
How it happens
MITRE describes the scenarios that produce it:
- A web application authenticates a user without first invalidating the existing session, and keeps using the session already associated with the user.
- An attacker is able to force a known session identifier on a user, so that once the user authenticates, the attacker has access to that session.
- The application or container uses predictable session identifiers.
In the generic exploit, the attacker creates a new session on the application and records its identifier. They then cause the victim to associate, and possibly authenticate, with the server using that identifier. How they push the ID onto the victim varies: a crafted link when IDs are accepted in the URL, or another way of setting a cookie.
Here is a vulnerable login handler in a Flask style app:
@app.route("/login", methods=["POST"])
def login():
user = check_credentials(request.form["u"], request.form["p"])
if user:
session["user_id"] = user.id # same session ID as before login
return redirect("/home")
The session cookie was issued before login. After login it is the same cookie, now with an authenticated identity attached. Anyone who knows that cookie value is logged in as the user.
The same bug in Java servlet code:
HttpSession session = request.getSession(); // reuses existing session
session.setAttribute("user", authenticatedUser);
A real case from the record: CVE-2022-2820, where website software for game servers did not properly terminate user sessions, allowing possible session fixation.
What an attacker gains
MITRE lists the consequence as access control: gain privileges or assume identity. The attacker becomes the victim for as long as the session lives. That can mean reading private data, changing account settings, or making purchases and transfers, whatever the real user could do. The victim notices nothing because their own login worked normally.
How to find it
The record lists no detection methods, but this weakness is easy to test by hand.
- Open the site in a fresh browser profile and note the session cookie value before logging in.
- Log in.
- Compare the cookie. If the value is unchanged, the application keeps the pre-login identifier and is exposed.
Repeat for every action that raises privilege: login, MFA completion, role switch, password change. Also check whether the app accepts session identifiers in the URL or from query parameters, and whether identifiers look sequential or guessable.
Code review is quick too. Find the login handler and look for the call that regenerates the session ID. If there is none, that is the finding. A dynamic scan of a running app can run the same before and after comparison automatically.
How to fix it
MITRE's mitigations are direct.
- Invalidate any existing session identifiers before authorizing a new user session.
- For platforms such as ASP that do not generate new session ID cookie values, use a secondary cookie. Set a random value in a second cookie and store the same value in a session variable. If the two ever do not match, invalidate the session and force a new login.
- Operationally, an application firewall that detects these attacks can help when the code cannot be fixed, for example when a third party controls it. MITRE rates its effectiveness as moderate and warns it may not cover all input vectors and can reject legitimate requests. Treat it as a stopgap, not the fix.
Fixed Flask version:
@app.route("/login", methods=["POST"])
def login():
user = check_credentials(request.form["u"], request.form["p"])
if user:
session.clear() # drop the old session data
session["user_id"] = user.id # framework issues a fresh ID
return redirect("/home")
Check that your framework really issues a new ID when the session is cleared. If it does not, rotate it explicitly with the framework's API.
Fixed Java version:
request.getSession().invalidate();
HttpSession fresh = request.getSession(true);
fresh.setAttribute("user", authenticatedUser);
Servlet containers also offer request.changeSessionId() for the same purpose.
Add a few supporting habits: never accept session IDs from URLs, set cookies with the Secure and HttpOnly flags, use identifiers generated by a cryptographically secure source, and expire sessions on logout on the server side, not just by deleting the cookie.
Try it yourself
Click through it at your own pace, or open it full screen.
Quick check
The app uses HTTPS everywhere. Is session fixation still possible?
Yes. HTTPS protects the identifier in transit, but fixation works by getting the victim to use an identifier the attacker already knows. The fix is to issue a new identifier at login.
What is the one line test for this bug?
Compare the session cookie before and after login. If it is the same value, the application is vulnerable.
Is a web application firewall enough?
MITRE rates it as moderate protection and suggests it as an emergency measure or defense in depth, not a replacement for invalidating the old session.