Session tokens: what a stolen cookie can do, and what limits it
A stolen session cookie is a logged-in user. Why an idle timeout does not bound the thief, and what absolute timeouts, rotation and device binding change.
A password protected by a second factor is a hard target. The cookie your server hands out one second after that second factor succeeds is not: it is a single string that means "this request is that person", and anything holding it is that person until something takes it away. This article covers what that string is worth to an attacker, which parts of the problem the browser can enforce for you, why the timeout most applications configure does nothing about a thief, and how much the exposure actually shrinks when you add an absolute timeout, rotation, or device binding. It ships a script that audits the Set-Cookie headers from your own site offline and computes the exposure window for a policy you describe, plus the dataset behind the chart.
The cookie is the account
MITRE ATT&CK catalogues the theft as T1539, Steal Web Session Cookie (version 1.5, last modified 12 May 2026), and the use as T1550.004, Web Session Cookie (version 2.0, same date). The first page states the premise plainly: "Web applications and services often use session cookies as an authentication token after a user has authenticated to a website." The second explains why this is worth an attacker's time even against accounts with multi-factor authentication: a session that already exists is already authenticated, so importing the cookie into a browser they control lets them act as the user without ever meeting the second factor. ATT&CK names adversary-in-the-middle phishing proxies, among them Evilginx2 and Muraena, as a route to collecting them, alongside malware that reads the browser's cookie storage.
The government view is the same. NIST and CISA published IR 8587 as an initial public draft in December 2025, "Protecting Tokens and Assertions from Forgery, Theft, and Misuse", with a comment period that closed on 30 January 2026. Its section 6.5 describes device-bound session credentials as technology "designed to help protect against cookies and session hijacking threats", aimed at the case where a session "has not shifted to a new, potentially compromised device or attacker who may have been able to compromise a session cookie or access token." The defensive question is therefore not whether the cookie can be taken, but how long it is worth anything once it has been.
What the browser will enforce for you
SP 800-63B-4, published in July 2025, gives seven requirements for cookies used for session maintenance in section 5.1. Cookies "SHALL be tagged to be accessible only on secure (i.e., HTTPS) sessions", "SHALL be accessible to the minimum practical hostnames and paths", "SHOULD be tagged as inaccessible via JavaScript (i.e., HttpOnly)", "SHOULD be tagged to expire at or soon after the session's validity period", "SHOULD have the __Host- prefix and set Path=/", "SHOULD set SameSite=Lax or SameSite=Strict", and "SHOULD contain only an opaque string (e.g., a session identifier) and SHALL NOT contain cleartext personal information." The session secret itself is "established using input from an approved random bit generator" and is "at least 64 bits in length"; OWASP's Session Management Cheat Sheet puts the same floor as "at least 16 hexadecimal characters long to achieve the required 64 bits of entropy."
The __Host- prefix is the item most often missing, and it is the one the browser polices. Section 4.1.3.2 of draft-ietf-httpbis-rfc6265bis-22, published 1 December 2025, defines it: "If a cookie's name begins with a case-sensitive match for the string __Host-, then the cookie will have been set with a Secure attribute, a Path attribute with a value of /, and no Domain attribute." That turns three conventions into a contract the user agent refuses to break, which matters because a Domain attribute widens the cookie to every subdomain: with Domain=site.example the agent sends it "to site.example, www.site.example, and www.corp.site.example", so any one of those hosts becomes a place your session can leak from.
Verify this from the headers rather than from the framework's documentation, because section 8.6 of the same draft points out that the server cannot tell afterwards: "A server cannot determine from the Cookie field alone when a cookie will expire, for which hosts the cookie is valid, for which paths the cookie is valid, or whether the cookie was set with the Secure or HttpOnly attributes." Capture your own response and read it:
curl -sSI https://your-own-site.example/ | grep -i '^set-cookie' > my-headers.txt
python session-tokens-stolen-cookie-flags-rotation.py --cookies my-headers.txt
Run against a deliberately mixed example, the audit prints the rule, the finding, and the document behind it:
SESSIONID (16-char value)
[SHALL ] Secure missing
cookies for session maintenance SHALL be accessible only on HTTPS sessions
[SHOULD] SameSite not set
[SHOULD] no __Host- prefix
__Host-sid (22-char value)
meets every rule in this script
remember_me (26-char value)
[REVIEW] persistent cookie
persistent for 43,200 minutes (720.0 h); longer than the 24 h overall
reauthentication timeout SP 800-63B-4 SHOULD apply at AAL2
[SHALL ] value may carry cleartext personal information
The timeout that does nothing
Most applications configure an idle timeout and stop there. OWASP states the flaw in one sentence: "if the attacker is able to hijack a given session, the idle timeout does not limit the attacker's actions, as they can generate activity on the session periodically to keep the session active for longer periods of time." An idle timeout measures whether someone is using the session, and the thief is someone. What bounds them is the absolute timeout, which OWASP describes as limiting "the amount of time an attacker can use a hijacked session and impersonate the victim user."
SP 800-63B-4 sets that ceiling by assurance level in section 5.2. At AAL1, "a definite reauthentication overall timeout SHALL be established, which SHOULD be no more than 30 days", and an inactivity timeout "MAY be applied but is not required". At AAL2 the overall timeout "SHOULD be no more than 24 hours" and the inactivity timeout "SHOULD be no more than 1 hour". At AAL3 "the overall timeout for reauthentication SHALL be no more than 12 hours", with an inactivity timeout that "SHOULD be no more than 15 minutes".
The window, computed
The script's second mode turns a policy into the longest window in which a stolen cookie still works. The model is deliberately simple: an inactivity timeout never counts as a bound, and the window is the smallest of the controls that do bound it, which are the overall reauthentication timeout, rotation with replay detection (bounded by how soon the real user comes back and collides with the rotated token), an out-of-band alert acted on, and a device-bound short-lived cookie. With nothing at all, the bound is the cookie's own Max-Age.
| Policy | Usable window | Bounded by |
|---|---|---|
| Idle timeout only, 30-day persistent cookie | 43,200 min | the cookie's Max-Age |
| AAL1 overall timeout | 43,200 min | reauthentication timeout |
| AAL2 overall timeout, 1 h inactivity | 1,440 min | reauthentication timeout |
| AAL3 overall timeout, 15 min inactivity | 720 min | reauthentication timeout |
| Office-day absolute timeout | 480 min | reauthentication timeout |
| AAL2 plus rotation with replay detection, user back within 1 h | 60 min | rotation |
| AAL2 plus an alert acted on within 30 min | 30 min | detection and revocation |
| Device-bound session, 10-minute refresh cookie | 10 min | device-bound cookie lifetime |
The spread between the widest and narrowest configuration is 4,320 times. Two things stand out. The AAL1 ceiling and a 30-day persistent cookie are the same number, so a policy written to AAL1 and no policy at all end in the same place. And the controls that get the window under an hour are not timeouts at all: they are rotation that detects a duplicate, an alert someone acts on, and binding.
Rotation, and what makes it bite
Regenerating the session ID is mandatory in one place regardless of the rest: OWASP requires it "after any privilege level change within the associated user session", above all at login, which is what prevents session fixation. Rotation on a timer is a different control, and on its own it does not help, because a thief who is using the session receives each new token as it is issued.
What makes rotation bite is replay detection. RFC 9700, the OAuth 2.0 security best current practice published in January 2025, requires that "refresh tokens for public clients MUST be sender-constrained or use refresh token rotation", and its refresh-token protection section treats the reuse of an already-rotated token as evidence of compromise, with the response being revocation of the whole family of tokens issued from that grant. Applied to sessions, the rule is: when a superseded session token arrives, do not silently reissue, end every session in that lineage and make the user authenticate again. That is what turns the victim's next ordinary page load into the end of the attacker's access, and it is the mechanism behind the 60-minute row in the table.
The other half of RFC 9700's sentence is sender-constraining, for which it points at mutual TLS and at RFC 9449, DPoP, published in September 2023. Its abstract: "This document describes a mechanism for sender-constraining OAuth 2.0 tokens via a proof-of-possession mechanism on the application level. This mechanism allows for the detection of replay attacks with access and refresh tokens."
Binding the session to a device
The browser-side version of the same idea is Device Bound Session Credentials. The W3C explainer states the goal as reducing "session theft by offering an alternative to long-lived cookie bearer tokens, that allows session authentication that is bound to the user's device": a key pair is created at sign-in, the server issues short-lived cookies, and the browser refreshes them by proving possession of the private key. Google's Chrome for Developers post, last updated 3 March 2026, says DBSC is available in Chrome 145 on Windows, that "Chrome on Windows protects these keys in hardware using the Trusted Platform Module (TPM)", and describes the outcome for a defender: "if an attacker is trying to use a stolen cookie on a different device without access to the TPM, you can deny the request."
SP 800-63B-4 already accommodates this in section 5.1, noting that such technologies "mitigate the risk of theft of session secrets by using cryptographic protocols that prove the possession of a session secret rather than using them as bearer tokens", and that session secrets used with proof of possession "MAY persist" — the one case where a long-lived session is the safer design rather than the lazier one.
What none of it stops
- The attacker who stays. The DBSC explainer is explicit that an attacker with ongoing access to a compromised user agent "will be able to continuously access fresh DBSC-controlled bearer tokens", and that sophisticated malware may use the secure element as a signing oracle. Binding converts an exfiltrated cookie into a local-presence requirement; it does not evict anyone.
- Use of the victim's own browser. ATT&CK T1185, Browser Session Hijacking (version 2.1), describes injecting into the browser to inherit its cookies and sessions or proxying traffic through it, which ATT&CK notes can also defeat the protection two-factor authentication provides. Device binding, IP checks and user-agent checks all see the legitimate device, because it is the legitimate device.
- Client-side timeouts. OWASP: "Session timeout management and expiration must be enforced server-side."
- Property binding as a guarantee. Binding a session to IP address or user agent aids detection, but OWASP is candid that a shared NAT, a corporate proxy or a copied user-agent string defeats it, and that these properties "cannot be used by web applications to trustingly defend against session attacks".
- The model itself. Every number in the table is an upper bound from arithmetic on a policy, not a measurement of an incident. Real detection times vary, and a user who does not return for a day does not trigger the rotation collision that day.
Detection is the part that survives all of this. ATT&CK's mitigation M1047 for T1539 asks you to "implement auditing for authentication activities and user logins to detect the use of stolen session cookies" and to "monitor for impossible travel scenarios"; its T1550.004 analytics look for session cookie reuse on unmanaged browsers and unfamiliar device fingerprints. Section 5.3 of SP 800-63B-4 is titled Session Monitoring for the same reason.
The dataset
_README for session-tokens-stolen-cookie-flags-rotation.csv, one row per modelled policy:
scenario: the policy described in wordsabsolute_minutes,idle_minutes: the overall reauthentication timeout and the inactivity timeout, in minutes; empty where the policy has nonerotation_minutes,victim_return_minutes: the rotation interval and the assumed time until the real user's next requestdetection_minutes: assumed time from theft to an alert being acted onbinding,cookie_ttl_minutes:devicewhere the session is bound to a device key, and the refresh cookie's lifetimebounding_control: which control produced the windowusable_minutes,usable_hours: the computed upper boundsource_tag,generated_at: the document the timeout values came from, and the date the row was produced (2026-09-16)
What to do this week
- Capture your own
Set-Cookieheaders and run the audit; fixSecure,HttpOnly,SameSiteand the__Host-prefix before anything else, because they cost a deploy and no design work. - Find the absolute timeout in your session configuration. If there is not one, that is the finding, and 24 hours is the number SP 800-63B-4 points at for an AAL2 service.
- Make a superseded session token revoke its lineage instead of quietly issuing a new one, and alert on it.
- Give users a page that lists active sessions with device and location, and a control that ends all of them.
- Treat an authenticated session appearing from a new device or an impossible location as an alert to work, not a report to file.
The password was never the last line; the cookie is. Everything above decides how many minutes a copy of it is worth.
Sources
- NIST, SP 800-63B-4 "Digital Identity Guidelines: Authentication and Authenticator Management", Sec. 5 Session (read 2026-09-16)
- NIST, publication record for SP 800-63B-4 (published July 2025; supersedes SP 800-63B of 03/02/2020)
- OWASP, "Session Management Cheat Sheet" (source file last changed 13 August 2026, "docs: qualify SameSite cookie guidance"; read 2026-09-16)
- MITRE ATT&CK, "T1539 Steal Web Session Cookie" (v1.5, last modified 12 May 2026)
- MITRE ATT&CK, "T1550.004 Use Alternate Authentication Material: Web Session Cookie" (v2.0, last modified 12 May 2026)
- MITRE ATT&CK, "T1185 Browser Session Hijacking" (v2.1, last modified 12 May 2026)
- IETF, draft-ietf-httpbis-rfc6265bis-22 "Cookies: HTTP State Management Mechanism" (1 December 2025)
- IETF, RFC 9700 "Best Current Practice for OAuth 2.0 Security" (January 2025), Sec. 2.2.2 and 4.14
- IETF, RFC 9449 "OAuth 2.0 Demonstrating Proof of Possession (DPoP)" (September 2023)
- Google, "Device Bound Session Credentials now available on Windows", Chrome for Developers (last updated 3 March 2026)
- W3C Web Application Security WG, "Device Bound Session Credentials: A Protocol for Protecting From Cookie Theft" explainer (read 2026-09-16)
- NIST and CISA, IR 8587 ipd "Protecting Tokens and Assertions from Forgery, Theft, and Misuse" (initial public draft, December 2025), Sec. 6.5