At a glance
- Controls
- OWASP API2:2023 · SOC 2 CC6.1 · ISO 27001 A.9.2
Logging out is a promise: the credential you were using no longer works. Many products keep that promise only on the device that pressed the button. The token is self-contained and signed, the server trusts the signature, and nothing checks whether the session behind it was ended. On the screen the user is signed out; on the wire the old token still opens every door.
Why it happens
Stateless tokens are convenient because the server does not have to remember anything: it verifies a signature and reads the claims. That convenience is the weakness. Logout clears the token from the browser, but a copy taken earlier is still valid until it expires, which can be hours or days. Password change, account suspension and role downgrade have the same gap unless each one explicitly invalidates tokens already in the wild.
How the attack unfolds
An attacker who captured a token, through a logged request, a shared device, a compromised integration or a browser extension, keeps using it after the victim believes they are safe. The victim changes their password precisely because they suspect trouble, and the change does nothing to the attacker’s copy. The window is the token’s remaining lifetime, and during it the account behaves exactly as if the attacker were the owner.
A password change that leaves the attacker’s token alive is a lock changed on a door the attacker already walked through.MATT research team
Why review misses it
The logout code looks complete: it deletes the cookie and redirects. Nobody tests the token after logout, because the assumption is that logging out ends the session. The defect only appears when you keep a copy of the credential and try it again, which is a test written from the attacker’s point of view.
Where it hides
What a reviewer can look for in the code and in the behaviour, before anyone proves it.
- Sessions are stateless tokens with no server-side record that can be marked revoked.
- Logout clears the client cookie but makes no call that invalidates the token server-side.
- A password change or role change does not rotate a signing secret or a per-user token version.
- Token lifetimes are measured in hours or days, with no short-lived access token and refresh split.
A real instance
Details altered to protect the customer. Pattern, timing and outcome are as found.
How MATT tests it
The proof runs against the deployed product, on every release, whoever wrote the code.
- 01Capture a valid token, then log out and replay the token against protected endpoints.
- 02Change the account’s password and replay the pre-change token.
- 03Downgrade or suspend the account and confirm the old token loses the removed privileges within the promised window.
- 04Measure how long a revoked credential keeps working, and check the same across every device and API client.
The evidence
- GET /api/me (after logout)200 OKPre-logout token still returns the full profile
- GET /api/me (after password change)200 OKOld token unaffected by the reset
- GET /api/me (after logout)401 UnauthorizedSession marked revoked server-side, token refused
- GET /api/me (after password change)401 UnauthorizedPassword change rotates the per-user token version
A revoked session must stop working everywhere within one minute.
Executable checkAfter logout, password change and account removal, a captured token must be refused by every protected endpoint within one minute, on every release.
Every rule MATT holds →What the fix looks like
- 01
Keep a revocation record
Track sessions server-side, or carry a per-user token version that a password or role change increments, so a single event ends every credential at once.
- 02
Split access from refresh
Issue short-lived access tokens against a revocable refresh token, so the longest a dead session can survive is minutes.
- 03
Prove the window
A revoked session must stop working everywhere within one minute, checked on every release.
A check that was never written is found by proving the deployed behaviour, fixed with the engineer who shipped it, and kept fixed by running the control on every change. What is ExploitOps? →