JWT Session Token Exposed in URL Query Parameter
After a successful SSO login, the application issued a signed JWT session token and
delivered it to the browser as a plaintext URL query parameter in a GET redirect. The
token landed in /app/members/?token=<jwt> and was accepted without
session binding, IP binding, or single-use enforcement. Pasting the captured URL into
a clean incognito browser with no existing cookies produced a fully authenticated session
— including administrative access — confirming complete account takeover with nothing
more than the URL.
Why a token in a URL is a different threat model
Secrets in request bodies or Authorization headers are transient — they travel once,
between two parties, and disappear. A token in a URL query parameter is a different
problem: the URL is recorded everywhere. Server access logs write it. The browser stores
it in history. Every proxy and reverse proxy in the chain logs the full request line.
If the page subsequently loads any external resource, the token appears in the
Referer header sent to that third-party origin — analytics scripts, CDN
assets, or widgets. A token with a long validity window, placed in a URL, is a credential
that has been written to half a dozen artefacts the user has no visibility into.
The proof of concept
# SSO callback delivers session token as a GET query parameter
GET /app/members/?token=<signed-jwt> HTTP/1.1
Host: app.acme-lab.test
# no session cookie required — token alone establishes the session
# same token replayed from isolated incognito browser — no prior cookies
GET /app/members/?token=<same-signed-jwt> HTTP/1.1
Host: app.acme-lab.test
Cookie: (empty)
HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=<new-session>; Path=/; HttpOnly
# authenticated session issued — full admin access confirmed
# second replay — accepted again, no invalidation on first use
# jti field: static string, not a unique UUID per issuance
# token valid until expiry regardless of how many times it is redeemed
The JWT payload carried a static jti field with a fixed string value
rather than a unique UUID per issuance. Without a unique jti the server
has no mechanism to track whether a token has already been redeemed — replay detection
is structurally absent.
Where the token leaks without attacker involvement
Because the token sits in the URL, it is recorded in at least four artefact classes
passively: server access logs at the application tier, browser history on the client
machine, proxy and WAF logs in the network path, and Referer headers sent
to any external origin the landing page loads. Any party with read access to any of these
artefacts can replay the token within its validity window and authenticate as the target
user — no active interception required.
SSO authentication flow captured in Burp. Callback redirect observed delivering token as a ?token= query parameter on the GET request to the application landing page.
Token URL pasted into isolated incognito browser. No cookies, no prior session. Full authenticated session issued — admin-level access confirmed from the server response.
Same token replayed a second time — accepted again. Static jti field confirmed. No invalidation on first redemption. Token remains valid until its expiry timestamp.
Token confirmed in server access logs and Referer header. External resource loaded by the landing page received the full token URL in the outbound Referer on page load.
Full finding documented. Replay evidence, leakage paths, and static jti value all captured with request/response evidence. Raised as High severity.
Fixing the token delivery
- Transmit the JWT via POST body or
Authorizationheader rather than a URL query parameter. The SSO callback should POST the token to the application rather than redirecting with it in the URL — this removes it from logs, browser history, and Referer headers in a single architectural change. - Implement single-use enforcement at the callback endpoint. Invalidate the token server-side immediately upon first successful redemption and reject any subsequent presentation with HTTP 401. A short-lived, single-use token eliminates the replay window even if the URL is captured.
- Replace the static
jtifield with a unique UUID generated per issuance. Store each redeemedjtivalue server-side and reject any token whosejtihas already been seen — this is the mechanism that makes single-use enforcement structurally possible. - Bind the token at issuance to the originating session context or source IP and validate this binding upon redemption. A token captured from one context should not be redeemable from a different device or network location within the same validity window.
The takeaway
A JWT in a URL is a credential in a log file. The session binding, replay enforcement, and leakage surface problems are independent — each is fixable alone — but they all stem from one root cause: the delivery mechanism. Move the token out of the URL and the rest of the attack surface collapses with it.