Two bad practices that are tolerated on their own and open the door together: the token travels in the URL, and the token never expires.
From a parameter in the address to an administrator account open to the internet
The first thing the review surfaced was not a coding mistake but a design decision: every request to the API carried the security token inside the address itself, as just another parameter. A call looked like this: http://api.example.com/me?token=<your_token>. The token is the key to the session, and it was travelling in plain sight of anyone able to read that address.
A token in the URL does not stay in the URL. It ends up written into server logs, saved in browser history and indexed by search engines, and it leaks from any of those three places. The CWE catalogue records the class as CWE-598, "Use of HTTP Request With Sensitive Query String". The OWASP Top 10:2025 puts it under A07, "Authentication Failures", among the conditions that make an application vulnerable: one that "exposes session identifier in the URL, a hidden field, or another insecure location that is accessible to the client".
The second finding was that those tokens never expired. Once issued, a token was good forever: no time limit, no invalidation after inactivity. On its own the defect opens no doors, which is why it tends to be parked behind more urgent work; it is CWE-613, "Insufficient Session Expiration", and the same OWASP category takes in applications that do not correctly invalidate sessions and authentication tokens on logout or after a period of inactivity, a line OWASP writes with single sign-on tokens mainly in mind. Combined with the first finding it stops being a detail, because it turns any old copy of an address into a credential that still works.
To find out whether this was theory or had already happened, the team watched the publicly reachable parts of the internet where an address of that kind could have been recorded. That is where the proof turned up: an API address with a real user’s token inside it, of the form http://api.example.com/analytics?token=<token>, already crawled and stored by a third-party web archive, the sort that keeps historical copies of public pages.
The token was tested against the application. It was still valid, it belonged to a real customer, and it carried administrator privileges. Put another way: anyone on the internet who reached that archived copy walked in as an administrator, without authenticating at any point and without typing a password.
RECONSTRUCTION · NO CLIENT DATA
How a token in the address became an administrator account
- CWE-598
- CWE-613
-
The design decision
GET /me?token=EXAMPLE-TOKEN-A1B2C3D4 HTTP/1.1 Host: api.example.com Accept: application/jsonThe session key, written into the request line.
Every call carried it. Not a coding mistake: a design decision.
-
A URL does not stay in the URL
-
SERVER LOG
"GET /me?token=EXAMPLE-TOKEN-A1B2C3D4 ..." -
BROWSER HISTORY
api.example.com/me?token=EXAMPLE-TOKEN-A1B2C3D4 -
PUBLIC CRAWLER
api.example.com/analytics?token=EXAMPLE-TOKEN-A1B2C3D4
It leaks from any of the three. The third one is outside the client's control.
-
SERVER LOG
-
The archived copy
THIRD-PARTY WEB ARCHIVE
http://api.example.com/analytics?token=EXAMPLE-TOKEN-A1B2C3D4A historical copy of a public page. It cannot be withdrawn at will.
-
Replayed
GET /analytics?token=EXAMPLE-TOKEN-A1B2C3D4 HTTP/1.1 Host: api.example.com Accept: application/jsonissuedNO EXPIRY · NO INACTIVITY TIMEOUT (CWE-613)
Time does not close this door.
Still valid. Real customer. Administrator.
No exploitation technique, no tooling. Opening an address that was already published was enough.
What we recommended
GET /me?token=EXAMPLE-TOKEN-A1B2C3D4 HTTP/1.1
GET /me HTTP/1.1
Host: api.example.com
Authorization: Bearer EXAMPLE-TOKEN-N7P4Q2R8
in the header it is neither logged nor indexed
Tokens get an expiry and a revocation path. Every token that has ever travelled inside a URL is treated as compromised and invalidated.
With that access, an attacker could add, edit or delete users of the system; read analytics and sensitive business data; insert malicious or fraudulent data; and read the users’ activity logs. No exploitation technique and no tooling were required: opening an address that was already published was enough.
The root cause is two decisions, and both are fixable: passing the token as a GET parameter, and giving it no expiry. What we recommended was to take the token out of the address and move it to the request’s authorisation header, where it is neither logged nor indexed; to give tokens an expiry and a revocation path; and, since a copy archived by a third party cannot be withdrawn at will, to treat as compromised and invalidate every token that has already travelled inside a URL.