Skip to content

Query-parameter bearer token (?token=) places credential in URIs — logs, history, Referer #420

Description

@forkwright

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions