⚠ Authorized testing only. Disclosed public bug-bounty data for defensive/educational research. Use payloads only against systems you are permitted to test.
LogoThe Hacktivity Field GuideReal-world web hacking, catalogued
🔎
Field Guide/Vulnerabilities/Session Management
Vulnerabilities

Session Management

§Basic information

A session is the credential that outlives the password — the cookie, bearer token, or magic link that proves "you already logged in." Every session control rests on two invariants: binding (a session belongs to exactly one authenticated user) and lifecycle (the session dies when it should — on logout, password change, revoke, or expiry). Break binding and you get session fixation; break lifecycle and a "revoked" credential keeps authenticating.

Because the session is the post-login identity, almost every session bug is an account-takeover primitive. The ceiling is forging the session outright: when a session is signed but not encrypted (Flask/itsdangerous, Rails cookie store, JWT) and the signing secret is default or leaked, you mint an admin session without ever touching a login form.

§Methodology

  1. Locate the session credential. Cookie (app.session, *.sid, _*_session), Authorization: Bearer, or a URL token (?login_token=, magic link). Note which channels use which — web cookie vs. mobile bearer vs. GraphQL are often authenticated by different code paths.
  2. Diff the login boundary. Capture the session id before auth, log in (or enter a share password), and compare. Unchanged value = fixation.
  3. Probe every lifecycle event by replay. Snapshot an authenticated request to a sensitive endpoint, perform the "invalidating" action (logout / password change / revoke / 2FA-enable), then replay the saved request. Still 200 + data = lifecycle broken.
  4. Fan the replay across channels. Revoke/logout is usually wired to only the primary web pipeline — retry the same revoked session against REST, GraphQL, websocket, and the mobile bearer independently.
  5. Assess token strength. Sample several tokens; look for structure (fixed increment, embedded user id, timestamp basis). Source-grep for weak RNG.
  6. Test cookie scoping. Retry every host/domain check with a trailing dot, mixed case, and percent-encoded name (see Bypasses).
# weak-token tell: time/PRNG-seeded session, reset, or CSRF tokens are predictable grep -REn 'uniqid|mt_rand|\brand\(|microtime' .

§Technique variants

Group your finding into one of these, then use the matching probe/payload.

Session fixation (login-boundary)

The pre-auth session id is reused after login instead of regenerated, so an id planted before login becomes an authenticated session. Any cookie-write primitive — shared machine, CRLF/cookie-injection, or a sibling-subdomain XSS that only needs to set a cookie — escalates to ATO.

# 1. Unauthenticated GET — record the assigned id GET / HTTP/1.1 Host: TARGET # -> Set-Cookie: app.session=PRE_AUTH_VALUE; Path=/ # 2. Log in (or enter the share password), then re-inspect. # If app.session is STILL PRE_AUTH_VALUE -> fixation.
// write a known unauth id domain-scoped into the victim, then have them log in over it document.cookie = '_flow_session=7b2f6c606fab4186d7be385aa66d53d9;domain=.TARGET;path=/'; // id not rotated at the OAuth/login callback -> replay it as the authenticated victim
● NOTE
Fixation is not only full-account login. Any unauth → auth transition must rotate the id: password-gated public share links, guest → member, and OAuth callbacks are all in scope. Diff the cookie across the specific boundary you're testing.

Broken logout / lifecycle

Log out, then replay a captured cookie/token against a sensitive endpoint. Still authenticated = logout was client-only. Watch for logout calls that error silently so the server skips revocation.

# logout aborts server-side on a missing param -> token never revoked DELETE /api/v1/logout HTTP/1.1 Host: accounts.TARGET Authorization: Bearer atkn_********** # -> {"error":"Missing Logout Token Hint"} # replay after "logout" — still 200 with PII GET /oauth/userinfo HTTP/1.1 Authorization: Bearer atkn_**********

Stateless-JWT / orphaned-token replay

A stateless JWT (Cognito/Auth0) has no server-side session store, so "logout" only drops it client-side — it authorizes until exp unless a denylist exists. Same shape for any derived credential (refresh token, remember-me, passwordless/magic login_token) that outlives its parent grant.

# decode exp, log out, replay a privileged call — still 200 POST /studio/invitations?tenantId=... HTTP/2 Host: api.TARGET Authorization: Bearer eyJraWQ...<cognito-id-token>... Content-Type: application/json {"email":"attacker@evil.com","role":"ADMINISTRATOR"}

Credential-change hooks

Password reset/change, 2FA enable, and SSO disconnect should tear down all other sessions. They frequently hook only one code path. Enumerate every entry point that mutates credentials — web form, CLI, admin API, bulk import, SSO sync — and test each for missing teardown.

# web /resetmypassword clears sessions, but the CLI path bypasses the hook airflow users reset-password --username VICTIM --password NEW # old sessions stay live

Forged / predictable tokens

When the session is signed client-side with a known/default secret, forge it directly. When it's generated from a non-CSPRNG, predict it.

# signed-but-not-encrypted session with a known SECRET_KEY -> forge a reset token for user_id 1 from itsdangerous import URLSafeTimedSerializer s = URLSafeTimedSerializer("SECRET_KEY") print(s.dumps("1")) # GET /reset/<forged> -> set new password -> admin
// predictable: session token derived from the server clock, not a CSPRNG $token = uniqid(); // microtime-based -> sample a few, brute the nearby timestamps

The browser enforces __Host-/__Secure- prefix rules and public-suffix (PSL) scoping on the literal, canonical name/host. If any server-side parser decodes the name or normalizes the host differently than the browser, a percent-encoded or trailing-dot/mixed-case variant slips the check yet matches at use time.

// browser enforces prefix rules on "__Host-"; the server URL-decodes "__%48ost-" to "__Host-" document.cookie = "__%48ost-evil=evil; domain=.TARGET"; // spoofs a "trusted" prefixed cookie
# trailing-dot FQDN accepted where the canonical public-suffix domain is rejected -> supercookie curl -b "" --connect-to foo.co.uk.:PORT:127.0.0.1:PORT \ --connect-to bar.co.uk.:PORT:127.0.0.1:PORT ... # Set-Cookie: trail=1; Domain=co.uk. -> later sent to unrelated bar.co.uk.

§Bypasses

Real logic/parser bypasses that defeated a session or cookie control:

Filter / controlBypassSeen in
"Revoke session" covers only webReplay the same session on the GraphQL API after revoke#417382
Invalidation wired to one code pathChange password via CLI (airflow users reset-password) — web-form teardown missed#3073507
Logout errors out silentlyDELETE /api/v1/logoutMissing Logout Token Hint; server skips revocation#1172205
Stateless-JWT "logout"No server denylist — bearer authorizes until exp#1319892
Revocation doesn't cascadePasswordless login_token survives parent OAuth-token revoke#172837
Logout only deactivates the idDormant session revives on next login, rebinds to the next user#250688
__Host-/__Secure- prefix trustPercent-encode the name (__%48ost-evil) — server URL-decodes, browser never enforced#895727, #1464396
Public-suffix (PSL) rejectTrailing-dot FQDN Domain=co.uk. accepted where co.uk rejected#3733905, #1565615
PSL reject (case-sensitive)Mixed-case Domain=co.UK — validation case-sensitive, matching case-insensitive#2212193
Cross-origin cookie isolationReused libcurl handle keeps stale cookiehost / Proxy-Digest state across host/proxy switch#3671818, #3697719
Session id unguessableCookie value is the raw user identifier — nothing to brute#178567
▸ TIP
The cookie-scoping rows share one principle: a check that normalizes differently than the later matcher is bypassable. Retry every host/domain/name check with a trailing . appended, uppercased, and percent-encoded.

§Escalation & impact

Session bugs are rarely the whole exploit — they are the amplifier that turns a "low" primitive into full ATO.

▲ WARNING
A logout that flips the UI to "logged out" proves nothing. The credential can live on server-side (client-only logout, stateless JWT, orphaned derived tokens). Always confirm with a replay against a sensitive endpoint on every channel — a 200 there is the finding, not the UI state.

§Prevention

§Tools

Specimens — real-world examples

The techniques above are the general method. Below, each disclosed HackerOne report is a catalogued example — concrete payload, outcome, and matching practice lab. 61 in this class.

Real-world example

Forge Flask/Redash session via known static SECRET_KEY -> ATO

◆ Critical
Specimen #1380121 · urbancompany · awarded · 123 votes · resolved
Program urbancompanySurface webChain known SECRET_KEY -> forged reset token/session -> log Tag account-takeover

Root cause

The Redash instance signed Flask sessions and password-reset tokens with a static/known secret. Knowing the secret lets an attacker forge signed tokens (e.g. itsdangerous URLSafeTimedSerializer) to reset any user's password or set an arbitrary session user_id, yielding admin access.

Method

  1. Determine the static REDASH_SECRET_KEY / REDASH_COOKIE_SECRET (default/leaked)
  2. Use itsdangerous with that secret to sign a password-reset token for user id 1
  3. Browse /reset/<forged-token> and set a new password to log in as that user (admin)
>>> from itsdangerous import URLSafeTimedSerializer >>> s = URLSafeTimedSerializer("<SECRET_KEY>") >>> s.dumps(str("1")) # forged reset token for user_id 1 # GET /reset/<forged> -> set new password -> admin login

Insight — When an app uses signed-but-not-encrypted client-side sessions (Flask/itsdangerous, Rails cookie store, JWT), a default or leaked secret is game over: forge sessions or reset tokens directly. Check for default framework secrets (Redash, Grafana, etc.) and test decoding/re-signing cookies offline.

Real-world example

SSO session not invalidated on logout (replay re-login)

◆ Critical
Specimen #222082 · owox · none · 17 votes · resolved
Program owoxSurface webTag account-takeover

Root cause

After logout the server-side session is not destroyed; replaying the old authenticated request, or clicking Sign in, restores access without re-entering Google/SSO credentials.

Method

  1. Sign in via Google SSO and browse
  2. Log out (also log out of Google)
  3. Click Sign in again / replay a prior authenticated request
  4. Access is restored with no credential prompt

Insight — Logout must invalidate the server session, not just clear the client; test old-request replay and re-click sign-in after logout, especially on SSO/OAuth-fronted apps.

Real-world example

Application accepts arbitrary session id to impersonate any user

◆ Critical
Specimen #1150573 · deptofdefense · none · 7 votes · resolved
Program deptofdefenseSurface webTag account-takeover

Root cause

The portal trusts a caller-supplied session identifier without binding it to the authenticated principal, so substituting another member's session id returns that member's data as if authenticated as them.

Method

  1. Authenticate to the portal with valid credentials and locate where the session id is supplied.
  2. Replace it with another member's session id.
  3. Observe access to that member's data as though authenticated as them.

Insight — Where a session id is passed in a parameter/field rather than only as an opaque server-bound cookie, try swapping it for another user's value. If the app resolves identity purely from the supplied id, it is session hijacking / IDOR-over-session-id, often critical.

Real-world example

Cookie __Host-/__Secure- prefix spoofing via name URL-decoding

◆ High
Specimen #1464396 · ibb · USD 2000 · 31 votes · resolved
Program ibbSurface other

Root cause

Ruby CGI::Cookie.parse applied URL-decoding to cookie NAMES. An attacker can send a percent-encoded name like __%48ost-evil that decodes to __Host-evil, injecting a value into a name that the browser/app trusts as prefix-protected (CVE-2021-41819, same class as CVE-2020-8184).

Method

  1. Send a cookie header with a URL-encoded prefix in the name plus a real prefixed cookie
  2. Server-side parser decodes the name, collapsing __%48ost- into __Host-
  3. The spoofed value is merged/returned for the protected cookie name
  4. App makes a security decision on an attacker-influenced __Host-/__Secure- cookie
Cookie: __%48ost-evil=evil;__Host-evil=abc # CGI::Cookie.parse(...)["__Host-evil"].to_a => ["evil","abc"]

Insight — Cookie prefixes (__Host-, __Secure-) are only trustworthy if the parser does NOT decode/normalize names. When auditing cookie parsers, test percent-encoded prefix names; fix is to never URL-decode cookie names.

Real-world example

Embedded browser forwards secure cookies across origin on redirect

◆ High
Specimen #1079561 · valve · awarded · 59 votes · resolved
Program valveSurface desktopChain cross-origin redirect -> login cookie leak -> account Tag account-takeover

Root cause

An in-app browser (Steam Big Picture mode) attaches secure login cookies to a request that begins at a trusted origin but is then forwarded/redirected to a different origin, leaking the cookies to that other site.

Method

  1. Get the embedded browser to open a trusted-origin URL that redirects cross-origin
  2. Observe secure/login cookies attached to the request as it is forwarded to the new origin
  3. Capture the leaked cookies on the attacker origin -> potential account takeover

Insight — Embedded/game/console browsers often mishandle cookie scoping on cross-origin redirects. Test WebViews/in-app browsers: start at a trusted origin that 302s to attacker.com and check whether Secure/HttpOnly session cookies follow. (From program summary; body limited-disclosure.)

Real-world example

Cross-proxy Digest auth state leak on reused libcurl easy handle

◆ Medium
Specimen #3697719 · curl · none · 37 votes · resolved
Program curlSurface otherTag account-takeover

Root cause

libcurl preserves per-easy-handle Proxy-Digest challenge state (realm/nonce) across transfer boundaries; after the proxy is switched, the first CONNECT to proxyB is built preemptively from proxyA's Digest state instead of starting unauthenticated.

Method

  1. On a reused easy handle, complete a CONNECT authenticated to proxyA (learns realm/nonce).
  2. Change the proxy to attacker-controlled proxyB and start a new transfer.
  3. Observe the first CONNECT to proxyB carries Proxy-Authorization: Digest built from proxyA's realm/nonce.
  4. Replay that leaked header to proxyA for the same CONNECT authority -> accepted 200 (real auth value); different authority -> 407.
CONNECT victim.test:18099 HTTP/1.1 Proxy-Authorization: Digest username="proxyuser", realm="proxyArealm", nonce="proxyAnonce", uri="victim.test:18099", ... // controls: fresh handle or curl_easy_reset() suppress the leak

Insight — When auditing HTTP client libraries, look for per-connection/credential state stored on a reusable handle that is not cleared across trust boundaries (proxy switch, host switch, transfer end). Reused handles crossing identity boundaries leak credentials.

Real-world example

Mixed-case domain bypasses PSL check to set supercookie

◆ Medium
Specimen #2212193 · curl · none · 28 votes · resolved
Program curlSurface other

Root cause

libcurl passes hostname and cookie domain to libpsl's psl_is_cookie_domain_acceptable without lowercasing them; an uppercase in the domain (e.g. domain=co.UK) evades the public-suffix rejection, while cookie matching later is case-insensitive, yielding a supercookie across the whole TLD (CVE-2023-46218).

Method

  1. Serve Set-Cookie with a public-suffix domain using mixed case (domain=co.UK) to a request whose Host uses matching mixed case
  2. curl accepts and stores the supercookie because the PSL check saw a non-normalized domain
  3. Request any other host under that suffix; the supercookie is sent
HTTP/1.1 200 OK Set-Cookie: super=oops; domain=co.UK # stored as: .co.UK TRUE / FALSE 0 super oops # then sent to any *.co.uk host

Insight — Look for case/normalization mismatches between the validation step and the use step. If a security check (PSL, allowlist, path, host compare) normalizes differently than the later matcher, a mixed-case or encoded variant slips past the check yet still matches at use time.

Real-world example

Failed XML-RPC login still issues a usable, non-invalidated session

◆ Medium
Specimen #3783738 · revive_adserver · none · 27 votes · resolved
Program revive_adserverSurface api

Root cause

Revive Adserver's XML-RPC ox.login returned an error but the HTTP response still set a valid session-id cookie and the session was never invalidated, so the leaked session id could drive subsequent authenticated API calls, bypassing the admin-only restriction.

Method

  1. Call the XML-RPC ox.login method (even with non-admin/invalid conditions)
  2. Method returns an error but response headers contain a Set-Cookie session id
  3. Reuse that session id for further API calls; they succeed without restriction
POST /www/api/v2/xmlrpc/ (method: ox.login) -> response: error body BUT Set-Cookie: <valid session id> -> reuse session id for authenticated API calls

Insight — Inspect responses to FAILED auth for tokens/cookies. An error status doesn't mean no session was created. Always test whether a session id issued on a failed/partial login is actually usable and whether the error path invalidates it.

Real-world example

Cookie set on TLD via trailing-dot host (curl CVE-2022-27779)

◆ Medium
Specimen #1565615 · ibb · awarded · 27 votes · resolved
Program ibbSurface networkTag cors

Root cause

libcurl (7.82-7.83.0, built without libpsl) rejected cookies for TLDs but the check failed when the host was given with a trailing dot (e.g. 'com.'), allowing a super-cookie shared across all sites under that TLD.

Method

  1. Target a curl-based client without libpsl
  2. Serve a Set-Cookie with domain matching a trailing-dot TLD host
  3. Cookie is now sent to unrelated sites under that TLD (session fixation)
curl 'http://example.com./' with Set-Cookie: X=Y; domain=.com.

Insight — Trailing-dot / case / IDN variants of a hostname routinely bypass public-suffix and domain-scoping checks; test them against cookie, CORS and SOP logic.

Real-world example

Predictable session tokens from PHP uniqid()

◆ Medium
Specimen #1306942 · revive_adserver · none · 14 votes · resolved
Program revive_adserverSurface webChain predictable token -> session hijack -> mass account taTag account-takeover

Root cause

Session tokens were generated with PHP uniqid(), which is derived from the server clock (microtime) and is not cryptographically secure, so tokens are low-entropy and predictable -> mass account takeover.

Method

  1. Locate token/session/reset generation and confirm uniqid()/rand()/mt_rand()/microtime() usage
  2. Sample a few tokens to learn the timestamp basis
  3. Predict/brute nearby timestamps to reconstruct valid session tokens for other users
// vulnerable: session token = uniqid(); (time-based, not CSPRNG)

Insight — Grep source for uniqid(), rand(), mt_rand(), microtime() feeding session tokens, password-reset links, or CSRF tokens - all predictable. The correct primitive is random_bytes()/CSPRNG. Predictable tokens escalate straight to ATO.

Real-world example

Stateless JWT bearer token cannot be revoked on logout

◆ Medium
Specimen #1319892 · trycourier · none · 12 votes · resolved
Program trycourierSurface apiTag jwtTag account-takeover

Root cause

The API authenticates with a stateless Cognito-issued JWT (Authorization: Bearer). Logout only discards it client-side; the backend has no revocation list, so the token keeps authorizing privileged actions until its exp claim.

Method

  1. Log in and capture a privileged API request carrying the JWT (e.g. POST /studio/invitations with Authorization: Bearer <jwt>).
  2. Log out of the web app.
  3. Replay the captured request with the same JWT and confirm it still returns 200 and performs the action (invite user / create workspace).
POST /studio/invitations?tenantId=... HTTP/2 Host: api.courier.com Authorization: Bearer eyJraWQ...<cognito-id-token>... Content-Type: application/json {"email":"attacker@yopmail.com","role":"ADMINISTRATOR"}

Insight — Decode the JWT (check exp) and replay it after logout. Stateless JWT auth (Cognito/Auth0) is commonly not revocable server-side, so 'logout' is meaningless until token expiry unless a denylist/short TTL + refresh rotation exists.

Real-world example

Cookie set for TLD + trailing dot leaks cross-site (curl CVE-2022-27779)

◆ Medium
Specimen #1553301 · curl · none · 5 votes · resolved
Program curlSurface other

Root cause

curl's cookie engine rejects cookies scoped to a bare TLD but not to TLD+'.' (e.g. Domain=.me.); since a trailing-dot host is treated as equivalent to the plain host, a cookie set for '.me.' is later sent to unrelated *.me sites.

Method

  1. Attacker site responds with Set-Cookie for Domain=<tld>. (trailing dot)
  2. curl stores it scoped to the trailing-dot TLD
  3. When curl requests any host under that TLD via trailing-dot form, the cookie is attached
  4. Cookie crosses to unrelated victim sites
# attacker response header: Set-Cookie: a=b; Domain=.me. curl -c cookies.txt http://localtest.me./index.php # cookies.txt: .me. TRUE / FALSE 0 a b curl -b cookies.txt http://victim.me./ # sends Cookie: a=b

Insight — Trailing-dot (FQDN root) hostnames bypass many domain/suffix checks (cookie scoping, SOP, host allowlists, Public Suffix List). Test appending '.' to hosts to defeat domain-equality logic and to smuggle cross-site cookies/routing.

Real-world example

Sessions not invalidated after MFA activation

◆ Medium
Specimen #667739 · superhuman · awarded · 176 votes · resolved
Program superhumanSurface webTag account-takeover

Root cause

Enabling 2FA/MFA does not invalidate previously established sessions on other devices; those sessions remain fully authenticated without ever passing the new second factor.

Method

  1. Log into the same account on devices A and B
  2. On A, complete 2FA activation
  3. On B, reload — session remains active without MFA challenge

Insight — Test session lifecycle around every security event: enabling MFA, password change, email change, logout-all. Pre-existing sessions should be revoked or re-challenged. This is a quick, high-signal check on any account-security settings page.

Real-world example

Disconnecting an SSO/social login provider does not revoke active session

◆ Medium
Specimen #1547684 · shopify · USD 1600 · 20 votes · resolved
Program shopifySurface webChain password compromise -> link attacker SSO -> disconnectTag oauthTag account-takeover

Root cause

Unlinking an external login provider (Google) from an account does not terminate sessions that were established via that provider, so an attacker who linked their Google account keeps access even after the victim disconnects it.

Method

  1. Attacker (having obtained the victim's password) links their own Google account to the victim's account and logs in via Google.
  2. Victim notices and disconnects the attacker's Google account.
  3. Attacker's existing session does not expire and still has access; attacker can re-link their Google account as a persistent backdoor.

Insight — Disconnecting/unlinking an SSO provider is a revocation event that many apps ignore. Test that removing a linked identity provider (Google/GitHub/SAML) kills sessions created through it; otherwise it is a backdoor that survives even password changes.

Real-world example

Web-push subscriptions persist per-account across logout/switch and leak data

◆ Medium
Specimen #347748 · twitter · USD 560 · 19 votes · resolved
Program twitterSurface webTag account-takeover

Root cause

Web push notification subscriptions are bound per-account and are not torn down on logout or account switch, so notifications (including DM contents) for a signed-out account keep arriving in the browser.

Method

  1. Log in to multiple accounts one by one and enable web push notifications.
  2. Log in to one account; have someone send a DM to a DIFFERENT (logged-out) account.
  3. Observe browser notifications for the logged-out account, exposing DM content; clicking sends that account's cookies alongside the other account's request.

Insight — After logout/account-switch, check whether push subscriptions, service workers, or notification tokens are revoked. Persistent push registrations are a data-leak channel that survives session teardown on multi-account clients.

Real-world example

Scope revocation doesn't invalidate the session cookie derived from the token

◆ Medium
Specimen #388515 · nextcloud · 100 · 17 votes · resolved
Program nextcloudSurface web

Root cause

Revoking an app token's filesystem-access scope does not invalidate the session cookie already minted from that token, so the live session keeps file access after the permission is revoked (CVE-2018-16466).

Method

  1. Obtain an app token
  2. Access files with it and save the resulting cookies
  3. Revoke filesystem access for that token
  4. Reuse the saved cookies - files are still accessible

Insight — After revoking a permission/scope/token, always re-test existing sessions and cookies derived from it. Revocation frequently only blocks new authentication, leaving live sessions fully privileged.

Real-world example

Long-lived sub-session (web terminal/exec) outlives parent session expiry

◆ Medium
Specimen #2123094 · ibb · USD 2540 · 9 votes · resolved
Program ibbSurface webTag account-takeover

Root cause

An interactive web-terminal/exec channel established during a valid session keeps operating (websocket to a live machine) after the parent web session has expired, because its lifetime is not tied to the session.

Method

  1. Log in to the application (ArgoCD).
  2. Open a web-terminal session that operates a backend machine.
  3. Wait until the main web session expires.
  4. Confirm the terminal/exec channel is still connected and usable.

Insight — Persistent channels (web terminals, websockets, SSE, long-poll, exec streams) often authenticate once at open time and never re-check session validity. Open such a channel, expire/revoke the parent session, and verify the channel dies.

Real-world example

Web session ID reused against admin-only XML-RPC API

◆ Medium
Specimen #3672641 · revive_adserver · none · 6 votes · resolved
Program revive_adserverSurface api

Root cause

Session IDs minted for the low-privileged web admin console were accepted by the XML-RPC API, whose authentication is normally restricted to admin users; the session was not bound to a context (web vs API), so a low-priv web session authenticated privileged API calls.

Method

  1. Log into the web console as a low-privileged user and capture the session ID
  2. Present that session ID to the XML-RPC API endpoint
  3. API accepts it and exposes admin-only operations/vulnerabilities

Insight — When one app exposes multiple auth surfaces (web UI + API/XML-RPC), test cross-surface session portability. Sessions should carry a context/audience so a low-trust session can't drive a high-trust interface.

Real-world example

Session revocation doesn't revoke desktop/mobile client access

◆ Medium
Specimen #165353 · nextcloud · none · 3 votes · resolved
Program nextcloudSurface webTag account-takeover

Root cause

Killing a session in the web UI only destroys the PHP web session. Desktop/mobile clients that authenticated with raw username+password silently re-create a session on the next request, and some client sessions never appear in the session list at all.

Method

  1. Log in on web, desktop client, and mobile client with the same account (username+password, not app-specific token).
  2. In web UI, revoke the desktop client's session.
  3. Continue syncing from the desktop client -> it keeps working with no re-auth prompt.
  4. Note the mobile client never shows up in the sessions tab, so it can't be revoked at all.

Insight — When testing 'log out other sessions'/'revoke device' features, verify the token is actually invalidated server-side AND that every client type is enumerable/revocable. Clients holding raw credentials (vs app-specific tokens) defeat session revocation; the fix is per-client app passwords.

Real-world example

Session not invalidated on logout; store password readable post-logout

◆ Low
Specimen #837729 · shopify · awarded · 157 votes · resolved
Program shopifySurface webTag account-takeover

Root cause

After a 'successful logout' the server session remained valid; browsing directly to the storefront-password page still returned the protected store password, showing logout did not destroy the server-side session.

Method

  1. Log in, then log out (confirm success message)
  2. Navigate directly to /accounts/passwords (or another authenticated URL)
  3. Sensitive data still returned -> session alive
GET https://<shop>.myshopify.com/accounts/passwords (after logout, still authorized)

Insight — Never trust the logout UI: replay an authenticated request after logout. If it still returns data, the server session/token was not invalidated.

Real-world example

Logout does not invalidate server-side token (client-only logout)

◆ Low
Specimen #1172205 · shopify · awarded · 63 votes · resolved
Program shopifySurface mobile-androidTag account-takeover

Root cause

The logout call fails server-side (here DELETE /api/v1/logout returns 'Missing Logout Token Hint') or only clears client storage, so the access/session token remains valid and any captured token keeps authenticating requests.

Method

  1. Capture the bearer/session token from a logged-in session (HTTP request or local storage such as shared_prefs/default_user.xml).
  2. Log out from the app.
  3. Replay an authenticated request (e.g. GET /oauth/userinfo) with the old token and confirm it still succeeds.
DELETE /api/v1/logout HTTP/1.1 Host: accounts.shopify.com authorization: Bearer atkn_********** -> {"error":"Missing Logout Token Hint"} (server never revokes) GET /oauth/userinfo HTTP/1.1 authorization: Bearer atkn_********** -> 200 with user PII

Insight — After logout, replay a previously captured token/cookie against a sensitive endpoint. If it still returns data, logout is client-only. Watch for logout requests that error out silently (missing hint/param) so the server skips revocation.

Real-world example

Session invalidation covers web password change but not the CLI path

◆ Low
Specimen #3073507 · ibb · awarded · 57 votes · resolved
Program ibbSurface webTag account-takeover

Root cause

The /resetmypassword web form terminates all existing sessions, but changing the same password via the admin CLI tool bypasses that hook and leaves sessions active.

Method

  1. Log in as the target user and keep the session alive.
  2. Change that user's password using the CLI/admin tool (not the web reset form).
  3. Replay the still-active session and confirm continued access despite the password change.
airflow users reset-password --username <user> --password <new> # sessions not cleared

Insight — Session-invalidation-on-credential-change is often wired to ONE code path (the web form). Enumerate every entry point that mutates credentials (CLI, admin API, bulk import, SSO sync) and test each for missing session teardown.

Real-world example

Session-revoke UI does not cover the GraphQL API channel

◆ Low
Specimen #417382 · security · USD 500 · 47 votes · resolved
Program securitySurface graphqlTag graphqlTag account-takeover

Root cause

The 'revoke session' feature invalidates the web session for normal POST/GET flows but the GraphQL endpoint continues to honour the revoked session, allowing sensitive queries after revocation.

Method

  1. While logged in, capture a sensitive GraphQL query (e.g. User_bounty_settings_page) and send it to Repeater.
  2. Go to settings/sessions and Revoke the current session; you get logged out of the web UI.
  3. Replay the GraphQL request from Repeater and confirm it still returns sensitive data.
POST /graphql {"query":"query User_bounty_settings_page(...){ me { ... bounties{edges{node{awarded_amount,report{title,team{handle}}}}} } }","variables":{...}}

Insight — Session revocation and logout are frequently enforced only on the primary web pipeline. Independently test every API surface (GraphQL, REST, websocket, mobile token) against a revoked/logged-out session.

Real-world example

Cookie-prefix (__Host-/__Secure-) spoofing via percent-encoded cookie name

◆ Low
Specimen #895727 · rails · none · 46 votes · resolved
Program railsSurface webTag account-takeover

Root cause

Rack unescaped cookie NAMES while parsing, so a cookie named __%48ost- decodes server-side to __Host-. The browser never enforced the __Host-/__Secure- integrity rules (Secure, HostOnly, path=/) on the encoded form, so an attacker on a subdomain could set a cookie the server parses as a trusted prefixed cookie.

Method

  1. From an attacker-controlled subdomain, set a cookie whose name is the percent-encoded prefix with a wildcard domain
  2. Browser sends __%48ost-evil to the app (no HostOnly enforcement since name != literal __Host-)
  3. Server-side unescape turns it into __Host-evil, which app logic trusts
document.cookie = "__%48ost-evil=evil; domain=.example.com"; // server: URI.unescape("__%48ost-evil") => "__Host-evil"

Insight — Never trust __Host-/__Secure- prefixes if any parser in the stack unescapes cookie names. Test percent-encoded prefix variants (__%48ost-, __%53ecure-) from a subdomain to forge 'trusted' cookies. Cookie name encoding is a parser-differential surface just like the value.

§References & practice

  1. PortSwigger Web Security Academy — Authentication labs (hands-on practice).
  2. All 61 disclosed reports for this class are catalogued as specimens above.
  3. See also: exploit chains · payload libraries · methodology.