Finding
The authentication middleware accepts a full-duration JWT bearer token via the ?token=<jwt> query parameter, placing the credential in the request URI. The token extracted from the query is forwarded to the same validate_bearer path as a header-borne token and yields a full access-token session — the only difference is that auth_method is tagged AuthMethod::QueryParam. That tag is never consumed to restrict scope, shorten lifetime, or sanitise logging, so the query path is functionally equivalent to header auth while exposing the secret in the URL.
Evidence
crates/exousia/src/middleware.rs:95
fn extract_query_token(parts: &Parts) -> Option<String> {
parts.uri.query().and_then(|q| {
q.split('&')
.find_map(|pair| pair.strip_prefix("token=").map(|v| v.to_string()))
})
}
The extracted value is forwarded directly to validate_bearer and issues a full session (crates/exousia/src/middleware.rs:126):
if let Some(token) = extract_query_token(parts) {
return service
.validate_bearer(&token)
.await
.map(|mut u| {
u.auth_method = AuthMethod::QueryParam;
u
})
.map_err(|_| unauthorized("invalid or expired token"));
}
The AuthMethod::QueryParam tag is set but is not used downstream to constrain TTL, endpoint scope, or logging.
Why this matters
A bearer token in the URL leaks through every system that records or forwards URIs: server access logs written to disk, browser history readable by any other user or process on the device, Referer headers sent to third-party origins of any embedded resource, and intermediary proxy/CDN logs. Under the counter-surveillance threat model the adversary is assumed capable of reading on-disk logs or sitting on the network path, so a single captured log line or Referer grants full user impersonation for the lifetime of the access token. Because the query token mints a normal full-duration session rather than a narrowly-scoped one, the blast radius of any such leak is the entire authenticated API surface, not a single read-only stream.
Desired correction
Eliminate the credential-in-URL exposure. Preferred: remove extract_query_token and require the Authorization: Bearer or X-Api-Key header for all clients. If a query-token path must remain for clients that cannot set headers (e.g. media/streaming <src> URLs), constrain it: gate it to an explicit allowlist of read-only/streaming endpoints, mint dedicated short-TTL tokens (e.g. <=60 s) for that path instead of full access tokens, and strip token= from any URI before it reaches a log sink. Consume the AuthMethod::QueryParam tag to enforce these restrictions rather than leaving it inert.
Done when: the ?token= path is either removed, or restricted to a documented allowlist of read-only/streaming endpoints using short-lived dedicated tokens with token= redacted from all logged URIs, and the AuthMethod::QueryParam tag actively enforces that restriction.
Finding
The authentication middleware accepts a full-duration JWT bearer token via the
?token=<jwt>query parameter, placing the credential in the request URI. The token extracted from the query is forwarded to the samevalidate_bearerpath as a header-borne token and yields a full access-token session — the only difference is thatauth_methodis taggedAuthMethod::QueryParam. That tag is never consumed to restrict scope, shorten lifetime, or sanitise logging, so the query path is functionally equivalent to header auth while exposing the secret in the URL.Evidence
crates/exousia/src/middleware.rs:95The extracted value is forwarded directly to
validate_bearerand issues a full session (crates/exousia/src/middleware.rs:126):The
AuthMethod::QueryParamtag is set but is not used downstream to constrain TTL, endpoint scope, or logging.Why this matters
A bearer token in the URL leaks through every system that records or forwards URIs: server access logs written to disk, browser history readable by any other user or process on the device,
Refererheaders sent to third-party origins of any embedded resource, and intermediary proxy/CDN logs. Under the counter-surveillance threat model the adversary is assumed capable of reading on-disk logs or sitting on the network path, so a single captured log line orReferergrants full user impersonation for the lifetime of the access token. Because the query token mints a normal full-duration session rather than a narrowly-scoped one, the blast radius of any such leak is the entire authenticated API surface, not a single read-only stream.Desired correction
Eliminate the credential-in-URL exposure. Preferred: remove
extract_query_tokenand require theAuthorization: BearerorX-Api-Keyheader for all clients. If a query-token path must remain for clients that cannot set headers (e.g. media/streaming<src>URLs), constrain it: gate it to an explicit allowlist of read-only/streaming endpoints, mint dedicated short-TTL tokens (e.g. <=60 s) for that path instead of full access tokens, and striptoken=from any URI before it reaches a log sink. Consume theAuthMethod::QueryParamtag to enforce these restrictions rather than leaving it inert.Done when: the
?token=path is either removed, or restricted to a documented allowlist of read-only/streaming endpoints using short-lived dedicated tokens withtoken=redacted from all logged URIs, and theAuthMethod::QueryParamtag actively enforces that restriction.