July 12, 2026
The Token That Wouldn’t Die
A high-severity bug I found by testing the one thing almost nobody checks
By Krypto
4 min read
Every bug hunter has that one report where the actual "hacking" takes ten minutes and writing it up takes three hours. This is one of those.
I was testing a SaaS platform that had recently added an AI chat assistant — the kind of feature every product seems to be bolting on right now. To make that work without handing the AI backend your full login session, the platform issued a separate, smaller token every time you opened the chat feature. A kind of hall pass, scoped just to the AI service.
That hall pass is what I went looking at. And it turned out nobody had checked one very obvious thing: what happens to the old pass once you get a new one?
Turns out: nothing happens to it. It just keeps working. Indefinitely, as far as I could tell.
Vulnerability:_ Improper AI access token revocation_
Severity:_ High_
Class:_ Broken Authentication / Session Management — OWASP API Security Top 10, API2_
Status:_ Confirmed by the vendor, fixed, and rewarded_
What's an "AI access token," anyway?
A lot of products are adding AI features on top of systems that weren't originally built for it. Instead of letting the AI backend trust your main session directly, the app issues you a separate, narrower token just for talking to the AI service:
GET /api/v2/core/ai-access-token
Authorization: Bearer <your normal session token>
{
"message": "success.",
"access_token": "AI_TOKEN_A"
}GET /api/v2/core/ai-access-token
Authorization: Bearer <your normal session token>
{
"message": "success.",
"access_token": "AI_TOKEN_A"
}That access_token is now its own independent bearer credential. It's opaque too — not a JWT — so there's no exp claim or anything else you could decode client-side to check when, or if, it expires. You just have to trust the server to manage its lifecycle. Which, in this case, it wasn't doing.
The bug, in one sentence
Generating a new AI token didn't invalidate the old one. Both stayed valid at the same time, indefinitely.
Proof of concept
Here's the exact flow, cleaned up:
1. Generate a token (Token A)
GET /api/v2/core/ai-access-token
Authorization: Bearer <valid user session
{ "message": "success.", "access_token": "AI_TOKEN_A" }GET /api/v2/core/ai-access-token
Authorization: Bearer <valid user session
{ "message": "success.", "access_token": "AI_TOKEN_A" }2. Use Token A against the AI chat endpoint
POST /chat?stream=yes
Authorization: Bearer AI_TOKEN_APOST /chat?stream=yes
Authorization: Bearer AI_TOKEN_A→ 200 OK. The AI responds normally.
3. Generate a brand-new token, same user, same session (Token B)
GET /api/v2/core/ai-access-token
Authorization: Bearer <same valid user session>
{ "message": "success.", "access_token": "AI_TOKEN_B" }GET /api/v2/core/ai-access-token
Authorization: Bearer <same valid user session>
{ "message": "success.", "access_token": "AI_TOKEN_B" }4. Go back and reuse Token A
POST /chat?stream=yes
Authorization: Bearer AI_TOKEN_APOST /chat?stream=yes
Authorization: Bearer AI_TOKEN_A→ Still 200 OK. Token A never expired, was never revoked, and had no idea Token B existed.
That's the whole exploit. No fuzzing, no chained bugs, no clever payloads. Generate, generate again, reuse the first one.
Expected behavior: old tokens get revoked when a new one is issued, or expire quickly, or are at least bound to a specific session or device.
Actual behavior: every token ever issued stays valid, forever, with no way to revoke one individually not even by logging out.
Why this matters
Because these are bearer tokens, possession is the only thing that matters. No session binding, no device check, nothing tying a token to where it came from. So if a token ever leaks — through logs, a misconfigured proxy, browser storage, a malicious extension, whatever — that leak is permanent:
- Whoever has it gets persistent, unauthenticated access to the AI API.
- They can burn the company's AI usage costs with no rate limit tied to "this session."
- Rotating your token doesn't help. The old one still works.
- Logging out doesn't help either. Nothing was revoked server-side.
That combination — low effort to exploit, high and lasting impact — is exactly why this landed as High severity rather than something more academic.
For what it's worth: everything above was tested only against my own account. No other users' data was touched, and nothing was done beyond what was needed to prove the token reuse worked.
Why this bug is so easy to find (and why so many people miss it)
This is the part I actually want you to take from this post more than the specific bug itself.
When most people test a "regenerate token" or "rotate API key" feature — QA engineers, pentesters, even bug bounty hunters — they test exactly one thing: does the new one work? Click regenerate, copy the new value, confirm it authenticates, move on.
Almost nobody goes back and checks the other half: does the old one stop working when it's supposed to?
That second question is the entire test. It takes about two minutes, needs nothing fancier than your browser's dev tools or Postman, and it consistently turns up High-severity findings — because token lifecycle bugs sit right at the foundation of how authentication works.
Here's the checklist I'd add to any test plan, for AI tokens or literally any credential system:
- Regenerate an API key or access token → try the old one afterward. Does it still authenticate?
- Change your password → does your existing session or token die, or keep going?
- Click "log out" → is the token actually revoked server-side, or just deleted from local storage while the value itself is still valid if replayed?
- Use "log out of all devices" → does it kill every issued token, or just the current one?
- Revoke a third-party app's OAuth access → does a previously cached token from that app still work?
- Deactivate or delete a user account → do tokens issued before deactivation keep functioning?
Every one of these is the same underlying pattern: the app assumes issuing something new implicitly cancels something old, without ever enforcing that server-side. It's an easy assumption to make while building the feature, and an easy step to skip while testing it — which is exactly why it keeps showing up.
If you're newer to bug bounty hunting and want a place to find consistent, high-impact bugs without needing deep exploit-dev skills, this is it. Go find every "regenerate," "rotate," "revoke," or "log out" button in an app and just check whether the old credential actually dies. You'll find this more often than you'd expect.
How I'd fix it
These were my recommendations in the original report, and they apply to basically any system with this problem:
- Revoke previously issued tokens whenever a new one is generated.
- Enforce short expiration windows on sensitive tokens — something like 5–15 minutes is reasonable for an AI session token.
- Bind tokens to the session, device, or context that requested them.
- Actually invalidate tokens server-side on logout, not just client-side.
- Track and cap concurrent active tokens per user, and alert on anomalies.
None of these are exotic. They're the same five fixes for basically every "old credential still works" bug, because the root cause is always identical: there's no server-side revocation list.
Takeaways
- Broken authentication bugs are consistently rated High or Critical, and they're often the easiest category to test for. That's not a coincidence — impact and complexity move in opposite directions here.
- "Does the new credential work?" is half a test. "Does the old one stop working?" is the other half, and it's the half almost everyone skips.
- Build token and session lifecycle checks into your standard test cases: regenerate, rotate, log out, deactivate. Replay the old credential every single time.
- A follow-up email isn't annoying. It's part of the process.
If you're testing your own product's auth right now, try this exact flow on it. Two minutes. You might not love what you find.
Thanks for reading if you've run into a similar bug, I'd genuinely like to hear about it in the comments.