Summary
The auth middleware falls back to reading the bearer token from a ?token= URL query parameter, and this fallback is attached to the entire protected route group rather than only the SSE log-stream route that needs it. Long-lived (24-hour) tokens can therefore appear in URLs on every protected endpoint. (CWE-598 Use of GET Request Method with Sensitive Query Strings.)
Where it is (verified against main @ 15f66d4)
- The middleware checks the
Authorization header and otherwise sets tokenString = c.Query("token"): internal/auth/basic.go:168-171 (the only Query("token") in the codebase). The comment cites EventSource/SSE compatibility.
- The middleware is applied to the whole protected group:
internal/api/router.go:196-197 (protected := base.Group("/api/v1"); protected.Use(authenticator.Middleware())), covering workspaces, jobs, registries, groups, admin, and remote routes - not just the stream.
- The only route that genuinely needs it is the SSE job-log stream
GET /jobs/:id/logs/stream (internal/api/handlers/job.go:82, route at router.go:250), because EventSource cannot set headers.
- Tokens are valid 24 hours (
internal/auth/basic.go:26).
Impact
JWTs can be written to browser history, reverse-proxy and load-balancer logs, APM/monitoring tools, screenshots, copied URLs, and referrer headers. A leaked token grants the user's application privileges until expiry or key rotation.
Remediation
- Accept query credentials only on the exact SSE endpoint, or replace it with
fetch() streaming using an Authorization header, or issue a single-purpose, job-bound, short-lived stream capability.
- Reject query tokens on every other route.
- Redact sensitive query parameters in all logging layers; shorten JWT lifetime and add revocation/refresh.
Acceptance criteria
?token= receives 401 on all ordinary protected endpoints.
- A stream token cannot access another job or any non-stream route, and expires within minutes.
- Access logs contain no bearer material.
Identified in a security assessment of v0.13 (commit be3187c), re-validated against main @ 15f66d4. Confidence: High.
Summary
The auth middleware falls back to reading the bearer token from a
?token=URL query parameter, and this fallback is attached to the entire protected route group rather than only the SSE log-stream route that needs it. Long-lived (24-hour) tokens can therefore appear in URLs on every protected endpoint. (CWE-598 Use of GET Request Method with Sensitive Query Strings.)Where it is (verified against
main@15f66d4)Authorizationheader and otherwise setstokenString = c.Query("token"):internal/auth/basic.go:168-171(the onlyQuery("token")in the codebase). The comment cites EventSource/SSE compatibility.internal/api/router.go:196-197(protected := base.Group("/api/v1"); protected.Use(authenticator.Middleware())), covering workspaces, jobs, registries, groups, admin, and remote routes - not just the stream.GET /jobs/:id/logs/stream(internal/api/handlers/job.go:82, route atrouter.go:250), because EventSource cannot set headers.internal/auth/basic.go:26).Impact
JWTs can be written to browser history, reverse-proxy and load-balancer logs, APM/monitoring tools, screenshots, copied URLs, and referrer headers. A leaked token grants the user's application privileges until expiry or key rotation.
Remediation
fetch()streaming using an Authorization header, or issue a single-purpose, job-bound, short-lived stream capability.Acceptance criteria
?token=receives401on all ordinary protected endpoints.Identified in a security assessment of v0.13 (commit
be3187c), re-validated againstmain@15f66d4. Confidence: High.