How sessions and tokens are stolen
Signed in without signing in
At 09:14, Riley, who works in Cedar Inc.'s finance team, signed in on what looked like Cedar's sign-in page. It was a relay at a look-alike address, login-cedar.example. Riley's password and authenticator-app code went straight through to Cedar's real identity provider at login.cedar.example, which checked both and issued a session cookie. The relay kept that cookie. Six minutes later, a browser at a hosting provider's address, 203.0.113.57, presented it to login.cedar.example, and Cedar's identity provider treated that browser as Riley. No password or code was asked for.
Once sign-in succeeds, the evidence that matters changes. The password and the code proved who Riley was once, at 09:14. Every request after that is recognized by the session, as Staying signed in describes. The access tokens that applications use to call APIs, introduced in Giving another application limited access, work the same way. Both are bearer credentials: whoever presents a valid one is treated as the party it was issued to. The server checks that the value is genuine and current, and, where it keeps a record it can look up, that the value has not been revoked. It does not check who is holding it.
That makes a session or token worth stealing in its own right. It already carries the result of every check the sign-in performed, multi-factor authentication included. From the moment it is issued, it passes through several places where a copy can be taken: the path from the identity provider to the person's browser, the device that stores it, the page that uses it, the requests that carry it, and the records that systems and people keep about those requests.
Copied from the device
Browsers store sign-in cookies so that they survive a closed tab or a restart, and desktop applications keep their tokens on disk for the same reason. That storage belongs to the person's account on the device, and anything running as that person can usually read it. Information-stealing malware relies on exactly this. It typically arrives disguised as something the person wanted, such as free copies of paid software, an attachment, or a fake update. Once running, it collects saved passwords, browser cookies, and stored tokens and sends them to whoever operates it. The collected sets are often sold, so the person who replays a session may not be the person who stole it.
Cookie attributes do not help here. HttpOnly keeps scripts in a web page from reading a cookie, Secure keeps it off unencrypted connections, and SameSite limits when other sites can cause it to be sent. All three are rules the browser applies to web content. Malware is not web content. It runs as the person, outside any page, and reads what the browser keeps for that person. Browsers that encrypt stored cookies make collection harder, but usually with keys the same user account can reach.
A stronger sign-in method does not change this either. Suppose Riley signed in with a passkey, which a relay cannot use on a look-alike address. The passkey would protect the sign-in. Malware that copies the cookie afterward never touches the sign-in at all: it takes what the sign-in produced.
Not every copy needs malware. A session left open on a shared computer, a kiosk, or a family laptop is available to whoever sits down next. That is one reason for the idle timeout described in Staying signed in, and why signing out has to end the session on the server instead of only clearing the browser.
Read by scripts on the page
The photo application lets people add captions to shared albums. Suppose one version displays captions without escaping them, so a caption containing script markup runs as code in the browser of everyone who opens the album. This is cross-site scripting (XSS). The injected script runs inside photos.example, with the same access as the application's own code.
What that script can reach depends on where the application keeps its credentials. If the page stores an access token in the browser's local storage or in a JavaScript variable, the script can read it and send it to a server the attacker controls. The token then works from anywhere until it expires. A session cookie marked HttpOnly cannot be read this way. The script can still send requests from the victim's page, and the browser attaches the cookie to them, so the attacker can act as the victim for as long as the page stays open. HttpOnly turns taking a copy into using the session in place, a real improvement but not a complete defense.
Injected script is not the only script on a page. Analytics, chat widgets, and other third-party code often run with the same access as the application's own code. If one of those suppliers is compromised, its script can collect tokens on every site that loads it.
The defenses come in layers. Escaping output and validating input prevent the injection itself. A Content Security Policy tells the browser which script sources to allow, which limits what an injected script can load and where it can send data. Keeping tokens out of script-readable storage limits what a successful injection can take. Some designs keep access tokens entirely on a server component and give the browser only an HttpOnly session cookie for that server, so the page holds no token to read. Backends for frontends describes that design.
Leaked along the way
Many tokens are never stolen in any dramatic sense. They are written down somewhere and later read by the wrong person. Here are two fictional requests from the photo application's web page to its API, carrying the same token in different places, each followed by the line it adds to the API server's access log.
Request
GET /albums/42/photos?access_token=demo-access-token-51 HTTP/1.1
Host: api.photos.example
Referer: https://photos.example/albums/42
Access log entry
192.0.2.10 - - [01/Oct/2026:09:12:44 +0000] "GET /albums/42/photos?access_token=demo-access-token-51 HTTP/1.1" 200 5120 "https://photos.example/albums/42"
An access log records the request line by default, and the request line includes the query string. The token is now in the log file and in every copy of it, from the backups to the excerpt someone downloads to investigate a slow request. Any proxy or load balancer between the browser and the API writes the same line to its own log.
Request
GET /albums/42/photos HTTP/1.1
Host: api.photos.example
Authorization: Bearer demo-access-token-51
Referer: https://photos.example/albums/42
Access log entry
192.0.2.10 - - [01/Oct/2026:09:12:44 +0000] "GET /albums/42/photos HTTP/1.1" 200 5120 "https://photos.example/albums/42"
The second entry is identical apart from the missing token. Access logs usually record only a few headers, such as Referer, and leave out Authorization unless someone configures them otherwise, so the header keeps the token out of the default records. That protection is easy to undo. A developer who turns on full header logging to debug a failure, or an error-reporting tool that captures the complete request when an exception occurs, writes the token to storage that was never meant to hold credentials. Code that records requests should remove Authorization headers, cookies, and token parameters before anything is written.
A token in the address of the page itself travels further still. The browser keeps the address in its history, it goes wherever someone pastes the link, and depending on the page's referrer policy, the browser can send the full address to other sites in the Referer header.
People leak tokens too. A customer attaches a screenshot of the browser's developer tools to a support ticket. An engineer pastes a failing request, headers included, into a team chat. A recording of the browser's network traffic, saved to help diagnose a problem, contains every cookie and token the page used. Ticket systems and chat histories are rarely protected the way a credential store is, and deleting the message later does not undo the leak. As Protecting credentials and messages explains, a leaked secret has to be invalidated or replaced.
Replaying what was taken
Replaying a stolen session or token means presenting the copy as if you were the party it was issued to. To the server, a replay looks like any other request. The cookie the attacker sent at 09:20 named a session that existed, had not expired, and had not been revoked, so Cedar's identity provider had no reason to refuse it. The attacker used it to register an authenticator app of their own, a change that Cedar's recent sign-in check allowed because, as Limiting what stolen access can do explains, a relayed session is always fresh.
Multi-factor authentication was not asked for again because, from the server's point of view, it had already happened. The session is the server's record that Riley completed the password and code at 09:14. Asking for them on every request would defeat the purpose of a session, so the server relies on the session instead, and a copy carries that reliance with it. An API treats a stolen access token the same way.
What the records can show is the context around a replay. Cedar's identity provider recorded the 09:14 sign-in as coming from 203.0.113.24, the relay's server at a hosting provider, while Riley's own session had been active since 08:41 from Cedar's office network, 198.51.100.0/24. The stolen session was then used from 203.0.113.57, another hosting address. Riley never held the session the relay captured, so the records show only one party using it. Malware leaves a different pattern. The owner keeps using the session from their usual device while the copy is used at the same time from another network and another browser.
None of these signals proves theft on its own. People travel, use VPNs, and move between office and mobile networks during a day. Combined, they become strong, as Detecting identity attacks shows. Shorter lifetimes and tokens that a copy alone cannot use reduce what a copy is worth, the subject of Limiting what stolen access can do. Token theft and replay follows the same problem for access tokens at an API.