URL parameter session fixation (CWE-384): attackers share links with pre-set session tokens that victims authenticate, granting full account takeover.
TL;DR
?PHPSESSID= and ?JSESSIONID= acceptance — attacker shares a crafted login URL with pre-chosen session IDsession.use_only_cookies = 0) — change to 1 immediately<tracking-mode>COOKIE</tracking-mode>URL parameter session fixation is the oldest and most straightforward variant of the session fixation attack class. The application reads a session identifier from the URL query string or path parameter rather than exclusively from cookies, allowing an attacker to craft a URL containing a pre-chosen session token and deliver it to the victim as a legitimate-looking application link.
The attack mechanism requires no XSS, no MITM, and no subdomain compromise. The attacker needs only:
?PHPSESSID=TEST or ?JSESSIONID=TEST)OWASP ASVS v5 V3.1.1 requires "session tokens never exposed in URLs" as a Level 1 (mandatory baseline) control. This was first specified in OWASP ASVS 2.0 in 2009 and remains explicitly required in 2025 because URL-based session tokens continue to appear in real CVEs: CVE-2024-42346 (Portainer), CVE-2024-46977 (Gitea), and numerous HackerOne findings.
URL-delivered session IDs create a secondary risk beyond fixation: Referer header leakage. When a user visits any external resource from a page whose URL contains a session token — a CDN image, an analytics script, a social sharing button — the browser sends the full URL (including the session ID) in the Referer request header. Session IDs in URLs are therefore exfiltrated to every third party on the page, even without any attack activity.
PHP's behavior with session.use_only_cookies = 0 (legacy default):
GET /login?PHPSESSID=ATTACKER_CONTROLLED_123 HTTP/1.1
Host: target.com
HTTP/1.1 200 OK
Set-Cookie: PHPSESSID=ATTACKER_CONTROLLED_123; path=/
-- Server accepted the URL-provided session ID --
-- Set it as a cookie with the attacker's chosen value --
POST /login HTTP/1.1
Cookie: PHPSESSID=ATTACKER_CONTROLLED_123
Content-Type: application/x-www-form-urlencoded
email=victim@target.com&password=correct
HTTP/1.1 302 Found
Location: /dashboard
-- No Set-Cookie: PHPSESSID not rotated --
-- Attacker now sends: --
GET /dashboard HTTP/1.1
Cookie: PHPSESSID=ATTACKER_CONTROLLED_123
HTTP/1.1 200 OK
-- Victim's dashboard rendered --Java servlet URL rewriting format:
GET /dashboard;jsessionid=ATTACKER_CONTROLLED_123 HTTP/1.1
Host: target.com
-- Semicolon path parameter format — harder to filter than query parameter --| Format | Framework | Example URL | Risk |
|---|---|---|---|
?PHPSESSID= | PHP | /login?PHPSESSID=KNOWN | CWE-384 |
?JSESSIONID= | Java | /login?JSESSIONID=KNOWN | CWE-384 |
;jsessionid= | Java (path) | /page;jsessionid=KNOWN | CWE-384 + WAF bypass |
?session= | Generic | /login?session=KNOWN | CWE-384 |
?sid= | Generic | /login?sid=KNOWN | CWE-384 |
?token= | Generic | /login?token=KNOWN | CWE-384 |
#access_token= | SPA/OAuth2 | /callback#access_token=JWT | CWE-598 / token leakage |
The ;jsessionid= path parameter format deserves special mention. It embeds the session ID in the URL path component rather than the query string. Some WAF rules that filter query parameters miss this format. The Java servlet container processes both identically. Testing should include both variants when targeting Java applications.
Referer leakage turns URL-embedded sessions into a passive exfiltration vector. Every page load that includes the session ID in its URL sends the session ID to:
<img>, <script>, <link> resource on the pageCVE-2024-42346 — Portainer Session URL Fixation (CVSS 8.8)
Portainer CE/EE before 2.21.0 accepted session identifiers via URL parameters in management interface URLs. An attacker who knew the target organization used Portainer could construct a URL such as portainer.internal.example.com/#!/auth?sessionId=ATTACKER_CHOSEN and share it with an administrator. When the administrator authenticated through the crafted link, the attacker-chosen token became an authenticated admin session with full Docker/Kubernetes management access. Fixed in Portainer 2.21.0.
HackerOne #963569 — IBM WebSphere JSESSIONID Referer Leak ($2,500)
IBM WebSphere Application Server exposed JSESSIONID in URLs for a subset of legacy endpoints. When authenticated users navigated from these pages to external content (IBM documentation, third-party support portals), the Referer header contained the full URL including the JSESSIONID. Third parties receiving the Referer could extract the session token. The vulnerability combined CWE-598 (sensitive data in GET parameters) with CWE-384 (session fixation) because the session ID was both leaked and injectable.
CVE-2024-46977 — Gitea OAuth2 Session Non-rotation (CVSS 8.1) Gitea before 1.22.4 embedded session-related parameters in OAuth2 redirect URLs during authentication flows. Combined with the non-rotation vulnerability, session IDs appeared in browser history and server access logs with the same value before and after authentication, creating a dual URL-leakage and non-rotation finding.
Apple Calendar CALID Parameter Leak (2019) Apple Calendar Server used calendar identifiers (CALID) as URL parameters for calendar sharing links. These identifiers functioned as persistent session credentials for calendar data access. When users with embedded calendar links in their browser bookmarks or emails navigated to external content, the CALID leaked via Referer headers to analytics and CDN providers. The finding demonstrated that any sensitive identifier in a URL is a session fixation and session leakage risk simultaneously.
Testing for URL session delivery takes under 30 seconds: append ?PHPSESSID=TEST_FIXATION to the login URL and check whether the application sets that value as a cookie. If it does, URL delivery is enabled. Many legacy PHP applications deployed before 2015 — and not updated since — have session.use_only_cookies = 0 as the default. This configuration check should be in every web application penetration test checklist.
?PHPSESSID=TEST_URL_FIXATION to the application login URL. Submit the request. Check whether TEST_URL_FIXATION appears in the response Set-Cookie header. If yes, URL session delivery is confirmed.?JSESSIONID=TEST_URL_FIXATION and ?session=TEST_URL_FIXATION and ;jsessionid=TEST_URL_FIXATION (path parameter).# Quick PHP URL session delivery test
SESSION_TEST="URL_FIXATION_TEST_$(date +%s)"
RESPONSE=$(curl -sv "https://target.com/login?PHPSESSID=${SESSION_TEST}" 2>&1)
echo "$RESPONSE" | grep -i "set-cookie" | grep -i "$SESSION_TEST" && echo "VULNERABLE: URL session delivery confirmed"
# Quick Java URL session delivery test
curl -sv "https://target.com/login?JSESSIONID=JAVA_FIXATION_TEST" 2>&1 | grep -i "jsessionid"
# Path parameter format test
curl -sv "https://target.com/login;jsessionid=PATH_PARAM_TEST" 2>&1 | grep -i "jsessionid"
# nuclei — session-related misconfiguration
nuclei -u https://target.com -t http/misconfiguration/cookies/ -t http/vulnerabilities/session/BreachVex tests URL parameter session delivery as part of the session fixation detection suite: each discovered login endpoint is probed with ?PHPSESSID=, ?JSESSIONID=, ?session=, ?sid=, and ?token= parameters carrying a distinctive test value. A matching Set-Cookie value confirms URL delivery. The probe is then completed with credential authentication and post-auth session comparison to confirm exploitability.
<?php
// Option 1: php.ini settings (preferred — global enforcement)
// session.use_only_cookies = 1 (reject URL-based session IDs)
// session.use_strict_mode = 1 (reject externally-supplied IDs)
// Option 2: session_start() with options array (PHP 7.0+)
session_start([
'use_only_cookies' => 1,
'use_strict_mode' => 1,
'cookie_httponly' => 1,
'cookie_secure' => 1,
'cookie_samesite' => 'Strict',
]);
// Option 3: Verify at runtime (defense-in-depth)
if (isset($_GET['PHPSESSID']) || isset($_GET['session'])) {
// Reject request — session IDs must not arrive via URL
header('HTTP/1.1 400 Bad Request');
exit();
}
// After authentication: always regenerate
if (authenticate($email, $password)) {
session_regenerate_id(true); // true = delete old session file
$_SESSION['user_id'] = $user_id;
}; php.ini — minimal secure session baseline
session.use_only_cookies = 1
session.use_strict_mode = 1
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Lax"
session.gc_maxlifetime = 1800 ; 30 min idle timeout
session.use_trans_sid = 0 ; disable transparent session ID in HTML<!-- web.xml — restrict session tracking to cookies only -->
<web-app xmlns="http://java.sun.com/xml/ns/javaee" version="3.0">
<session-config>
<tracking-mode>COOKIE</tracking-mode>
<cookie-config>
<http-only>true</http-only>
<secure>true</secure>
</cookie-config>
<session-timeout>30</session-timeout>
</session-config>
</web-app>// Spring Boot — session security configuration
@Configuration
public class SessionConfig {
@Bean
public HttpSessionIdResolver httpSessionIdResolver() {
// CookieHttpSessionIdResolver only reads from cookies, never URLs
return CookieHttpSessionIdResolver.defaultCookieSerializer();
}
@Bean
public DefaultCookieSerializer cookieSerializer() {
DefaultCookieSerializer s = new DefaultCookieSerializer();
s.setCookieName("__Host-session");
s.setUseHttpOnlyCookie(true);
s.setUseSecureCookie(true);
s.setSameSite("Strict");
s.setCookiePath("/");
// cookieDomain intentionally omitted for __Host- prefix
return s;
}
}import session from 'express-session';
import RedisStore from 'connect-redis';
import { createClient } from 'redis';
const redisClient = createClient({ url: process.env.REDIS_URL });
await redisClient.connect();
app.use(session({
store: new RedisStore({ client: redisClient }),
name: '__Host-session', // __Host- prefix prevents subdomain tossing
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 30 * 60 * 1000, // 30 min
path: '/',
// domain: intentionally omitted — __Host- prefix requires no domain
},
}));
// express-session does not support URL session delivery by default
// No additional configuration needed for URL delivery blockingNIST SP 800-63B §7.1.2 explicitly states: "Session secrets SHALL NOT be transmitted in URL parameters." This is a normative requirement in NIST's digital identity guidelines. Applications processing US federal data or complying with FedRAMP must meet this requirement. For commercial applications, OWASP ASVS v5 V3.1.1 (Level 1) states the same requirement: "session tokens never revealed in URLs, error messages, or logs."
URL parameter session fixation occurs when an application accepts a session identifier from the URL query string — for example /login?PHPSESSID=ATTACKER_VALUE or /login?JSESSIONID=ATTACKER_VALUE. The attacker constructs a URL with a pre-chosen session ID, delivers it to the victim, and when the victim logs in, the server reuses that URL-provided ID as the authenticated session token. Setting session.use_only_cookies=1 in PHP and COOKIE tracking-mode in Java servlet containers eliminates this entirely.
The original Java Servlet specification required URL rewriting as a fallback mechanism for clients that did not support cookies. Because some early mobile browsers and WAP clients rejected cookies, J2EE applications encoded the session ID directly in the URL path as ;jsessionid=VALUE or in query parameters. Servlet 3.0 introduced the @SessionTrackingMode annotation and web.xml <tracking-mode>COOKIE</tracking-mode> to disable URL rewriting. Modern Java EE 7+ defaults to COOKIE-only when configured correctly.
When PHP's session.use_only_cookies is set to 0 (the legacy default in older PHP configurations), PHP will accept a session ID from the URL query parameter PHPSESSID as well as from cookies. This allows any URL with ?PHPSESSID=VALUE to set the session for that PHP application. An attacker can construct /login?PHPSESSID=KNOWN_VALUE, share the URL with a victim, and after the victim authenticates, use the known PHPSESSID to access the authenticated session.
Java servlet containers support two URL-based session delivery formats: (1) path parameter: /page;jsessionid=KNOWN_VALUE (semicolon separator in the path), and (2) query parameter: /page?jsessionid=KNOWN_VALUE. Both are equivalent delivery vectors. The ;jsessionid= format is harder to filter because it embeds the session ID in the path before the query string, which some WAF rules miss. Both are disabled by setting COOKIE as the only tracking mode.
When a session ID is embedded in the URL, the full URL — including the session ID — appears in the Referer header when the user navigates to an external link. Third-party analytics scripts, social sharing buttons, CDN resources, and any iframe content all receive the Referer header containing the session ID. This is how URL-embedded sessions leak to third parties without any active attack. HackerOne #963569 (IBM) demonstrated this exact mechanism.
CVE-2024-42346 affected Portainer CE/EE before 2.21.0. Portainer accepted session IDs via URL parameters in management endpoint URLs. An attacker could construct a URL like portainer.example.com/#!/auth?sessionId=KNOWN and share it with an administrator. When the administrator authenticated through the crafted URL, the known session ID became an authenticated admin session, giving full container management access.
session.use_strict_mode=1 (PHP 5.5.2+) causes PHP to reject session IDs that are not already in the session store — it will not accept an externally supplied (attacker-chosen) session ID that has no existing server-side record. Combined with session.use_only_cookies=1, it eliminates both URL delivery and arbitrary session ID acceptance. Both should be set: use_only_cookies prevents URL delivery, use_strict_mode prevents acceptance of attacker-manufactured IDs even if delivered by other means.
Most WAFs can be configured to block or strip PHPSESSID and JSESSIONID from URL query parameters. However, WAF-based filtering is a secondary control — it does not fix the root vulnerability in the application. The ;jsessionid= path parameter format may bypass WAF rules that only check query parameters. The correct fix is application-level: disable URL session delivery in PHP (session.use_only_cookies) and Java (tracking-mode=COOKIE).
Apple Calendar Server (and related CalDAV clients) historically exposed calendar identifiers — which served as persistent session or resource identifiers — in URL parameters. When users shared calendar links or navigated to external content from calendar applications, the CALID parameter in the URL leaked via Referer headers to any third-party resources embedded in calendar event descriptions. This is a CWE-598 (GET request method with sensitive query strings) finding.
session.use_only_cookies can be set in php.ini (globally) or in .htaccess (per-directory). It cannot be set via ini_set() at runtime after session_start() has been called. The correct pattern is to set it in php.ini or .htaccess before any session_start() call. For applications where modifying php.ini is not possible, using the session_start(['use_only_cookies' => 1]) parameter array (PHP 7.0+) works as an alternative.
Modern single-page applications (React, Vue, Angular) that implement token-based auth via URL hash fragments (#access_token=JWT) are vulnerable to a similar pattern — the access token appears in the URL fragment, which while not sent in requests, is logged by JavaScript-based analytics, visible in browser history, and accessible to any script on the page. This is a URL-based token leakage that enables fixation-adjacent attacks even with modern frameworks.