Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
HTTP is stateless, so every authenticated web application needs a mechanism to remember that a given request belongs to an already-logged-in user — a session, almost always represented by a cookie holding an opaque identifier or a signed token. Session management is a deceptively simple-looking piece of infrastructure that, done wrong, undermines every other authentication control the application has: strong passwords and MFA are irrelevant if the session that follows successful login can be predicted, stolen, or planted in advance.
Session fixation is the classic example of the last failure mode: rather than stealing an existing authenticated session, the attacker *primes* a session identifier before the victim ever logs in, and if the application does not issue a fresh identifier at authentication time, the attacker’s pre-chosen identifier becomes valid the moment the victim signs in — because it is the same identifier the attacker already holds.
Attack Prerequisites
For classic session fixation to work, all of the following generally need to be true:
- The application accepts a session identifier before authentication and continues using that same identifier after login, rather than regenerating it.
- The attacker can set or predict the victim’s session identifier — via a URL parameter, a cookie set through a related subdomain (“cookie tossing” when
Domainis scoped broadly), a low-entropy generation scheme, or a reflected XSS bug elsewhere on the same origin. - The victim can be induced to authenticate using the attacker-supplied session — a crafted link, or simply waiting, since many users authenticate to sites they already have a tab open to.
- No additional session-binding control (device fingerprint, IP consistency check) that would flag the same session ID suddenly being used from two different contexts.
How It Works
The mechanism has three steps. First, the attacker obtains a valid, *unauthenticated* session identifier from the target application — many apps issue a session cookie on the very first request, before any login occurs. Second, the attacker gets that same identifier into the victim’s browser: sending a link containing the session ID as a URL parameter (https://app.example/login?sessionid=ATTACKER123), or, where cookie scoping is loose, setting the cookie directly via a sibling subdomain the attacker controls. Third, the victim logs in normally — but if the application’s login handler simply marks the *existing* session as authenticated rather than issuing a brand-new identifier, the attacker’s copy of that same session ID is now also authenticated as the victim, since server-side session state, not the identifier itself, is what changed.
This is distinct from session hijacking, where an attacker steals an identifier that is already authenticated (via XSS, network sniffing on an unencrypted connection, or a leaked log). Fixation requires no theft at all — the attacker’s identifier was valid and known to them from the start; the only thing that changes is its authentication state, which the victim unknowingly grants by logging in.
The fix is architecturally simple and is exactly what OWASP’s Session Management Cheat Sheet prescribes: treat authentication, and any privilege-level change, as an event that invalidates the old session identifier and issues a completely new one. If the attacker’s pre-chosen ID stops being valid at the moment of login, fixation has nothing left to exploit.
Vulnerable Code / Configuration
A PHP login handler that authenticates the user but never rotates the session ID — session_start() reuses whatever session ID the request already carried, including one the attacker chose:
<?php
session_start();
// VULNERABLE: authenticates within the existing session without
// regenerating the session ID -- if this ID was attacker-supplied
// (via a link or cookie set before login), it is now authenticated.
if (verify_credentials($_POST['user'], $_POST['pass'])) {
$_SESSION['user_id'] = get_user_id($_POST['user']);
$_SESSION['authenticated'] = true;
}
PHPFixed: regenerate the session ID immediately on successful authentication, deleting the old session server-side:
<?php
session_start();
if (verify_credentials($_POST['user'], $_POST['pass'])) {
session_regenerate_id(true); // true = delete the old session
$_SESSION['user_id'] = get_user_id($_POST['user']);
$_SESSION['authenticated'] = true;
}
PHPThe equivalent gap in a Node/Express app using express-session — the session object is reused across the login boundary instead of being regenerated:
// VULNERABLE
app.post('/login', async (req, res) => {
const user = await authenticate(req.body.user, req.body.pass);
if (user) {
req.session.userId = user.id; // same session ID as before login
return res.redirect('/dashboard');
}
res.status(401).send('Invalid credentials');
});
// FIXED
app.post('/login', async (req, res) => {
const user = await authenticate(req.body.user, req.body.pass);
if (user) {
req.session.regenerate(err => {
if (err) return res.status(500).end();
req.session.userId = user.id; // new session ID issued here
res.redirect('/dashboard');
});
return;
}
res.status(401).send('Invalid credentials');
});
JavaScriptWalkthrough / Exploitation
Testing for session fixation as an assessor is a straightforward before/after comparison:
# 1. Capture the pre-auth session cookie.
curl -i https://app.example/login | grep -i 'set-cookie'
# Cookie: session=ABC123...
# 2. Authenticate while presenting that SAME cookie value.
curl -i -b 'session=ABC123...' -d 'user=victim&pass=Passw0rd!' \
https://app.example/login
# 3. Check whether the Set-Cookie response after login issues a NEW
# session value, or reuses ABC123. If it is reused, the
# application is vulnerable: anyone holding ABC123 before step 2
# is now also authenticated as victim.
BashIf a URL- or parameter-based session ID is supported at all (an older pattern, still occasionally seen for compatibility with cookie-disabled clients), demonstrate the full attack chain by crafting a link containing an attacker-chosen ID, sending it, and confirming the attacker’s browser session becomes authenticated once the victim clicks it and logs in.
Note: Cookie scoping bugs compound fixation risk even without XSS: a cookie set with
Domain=.example.comis readable and settable from every subdomain, so a lower-trust subdomain (a marketing site, a staging environment) can plant a session cookie that the main application will accept — this is sometimes called cookie tossing. The__Host-cookie prefix defends against this by requiringSecure, forbidding aDomainattribute, and requiringPath=/, which locks the cookie to the exact host that set it.
Opsec: When demonstrating fixation in an authorized test, use a test account you control as the “victim” step rather than inducing a real user to authenticate — the finding is fully provable with a before/after cookie comparison alone.
Detection and Defense
- Regenerate the session identifier on every authentication event and every privilege-level change (login, logout, password change, role/permission elevation) — per OWASP Session Management guidance, this is the primary fixation defense.
- Use
__Host--prefixed cookies where possible, plusSecure,HttpOnly, andSameSite=StrictorLax. - Generate session identifiers with at least 128 bits of entropy from a cryptographically secure random source, never sequential or predictable values.
- Do not accept session identifiers via URL parameters; cookies with the flags above are the only supported transport.
- Enforce idle and absolute session timeouts, and fully invalidate sessions server-side on logout rather than only clearing the client-side cookie.
Real-World Impact
Session fixation and related session-handling failures have been a persistent entry in OWASP’s authentication-and-session guidance for over a decade — historically its own category (A2 Broken Authentication in the 2017 Top 10) and now folded into A07:2021 Identification and Authentication Failures. It remains most common in legacy applications built on default framework session handling that was never explicitly configured to regenerate identifiers at login, a gap that is easy to introduce and easy to miss without targeted testing.
Conclusion
Session fixation exploits a single missing step — failing to issue a fresh session identifier at the moment trust changes — and is fully closed by that one step done consistently at login, logout, and every privilege change. Combined with strong cookie scoping (__Host-, Secure, HttpOnly, SameSite) and high-entropy identifiers, it is one of the more completely solvable classes of session-management vulnerability, provided the fix is applied everywhere trust is established, not just at the primary login form.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments