Add HealthCheck middleware (full rescan) - #4
Conversation
| func isHealthCheckRequest(paths, userAgents map[string]struct{}, req *http.Request) bool { | ||
| if _, ok := paths[req.URL.EscapedPath()]; ok { | ||
| return true | ||
| } | ||
| if _, ok := userAgents[req.Header.Get("User-Agent")]; ok { | ||
| return true | ||
| } | ||
| return false | ||
| } |
There was a problem hiding this comment.
Authentication Bypass via User-Agent-based Health Check in Reverse Proxy / Forward Auth Deployments
The oauth2-proxy application implements a health check middleware (pkg/middleware/healthcheck.go) that globally intercepts all incoming HTTP requests before any routing or authentication handlers are executed.
If the incoming request's User-Agent matches any of the configured health check User-Agents (such as "GoogleHC/1.0" when GCPHealthChecks is enabled, or the user-configured PingUserAgent), the middleware immediately returns 200 OK with the body "OK" and short-circuits the handler chain.
In forward-authentication or reverse-proxy auth architectures (such as Nginx auth_request, Traefik ForwardAuth, or Envoy ExtAuthz), the reverse proxy delegates authentication decisions to oauth2-proxy by forwarding client requests (including client headers like User-Agent) to an auth endpoint (e.g., /oauth2/auth). An unauthenticated attacker can set their User-Agent to a configured health check User-Agent, causing oauth2-proxy to return 200 OK. The reverse proxy interprets this as a successful authentication and grants the attacker access to the protected resource.
Furthermore, by default, opts.PingUserAgent is an empty string (""). Because the application does not filter out empty strings when constructing the trusted User-Agent set, any request that lacks a User-Agent header (or has an empty one) will match "" and bypass authentication entirely by default.
Steps to Reproduce
- Deploy
oauth2-proxyin a forward-authentication setup (e.g., using Nginxauth_requestpointing to/oauth2/auth). - Send an HTTP request to a protected endpoint (e.g.,
/admin) with either:
a. TheUser-Agentheader set to a configured health check User-Agent (e.g.,GoogleHC/1.0if GCP health checks are enabled).
b. NoUser-Agentheader at all (ifping-user-agentis left at its default empty value). - The health check middleware will match the
User-Agentand immediately return200 OK. - Nginx will interpret the
200 OKresponse as successful authentication and forward the request to the upstream service, bypassing authentication entirely.
# Bypass authentication when GCPHealthChecks is enabled
curl -H "User-Agent: GoogleHC/1.0" https://protected-app.example.com/admin
# Bypass authentication by default (when no PingUserAgent is configured, matching the empty string)
curl -H "User-Agent:" https://protected-app.example.com/adminFix with AI
A security vulnerability was found by Hacktron.
File: pkg/middleware/healthcheck.go
Lines: 40-48
Severity: critical
Vulnerability: Authentication Bypass via User-Agent-based Health Check in Reverse Proxy / Forward Auth Deployments
Description:
The `oauth2-proxy` application implements a health check middleware (`pkg/middleware/healthcheck.go`) that globally intercepts all incoming HTTP requests before any routing or authentication handlers are executed.
If the incoming request's `User-Agent` matches any of the configured health check User-Agents (such as `"GoogleHC/1.0"` when `GCPHealthChecks` is enabled, or the user-configured `PingUserAgent`), the middleware immediately returns `200 OK` with the body `"OK"` and short-circuits the handler chain.
In forward-authentication or reverse-proxy auth architectures (such as Nginx `auth_request`, Traefik `ForwardAuth`, or Envoy `ExtAuthz`), the reverse proxy delegates authentication decisions to `oauth2-proxy` by forwarding client requests (including client headers like `User-Agent`) to an auth endpoint (e.g., `/oauth2/auth`). An unauthenticated attacker can set their `User-Agent` to a configured health check User-Agent, causing `oauth2-proxy` to return `200 OK`. The reverse proxy interprets this as a successful authentication and grants the attacker access to the protected resource.
Furthermore, by default, `opts.PingUserAgent` is an empty string (`""`). Because the application does not filter out empty strings when constructing the trusted `User-Agent` set, any request that lacks a `User-Agent` header (or has an empty one) will match `""` and bypass authentication entirely by default.
Proof of Concept:
**Steps to Reproduce**
1. Deploy `oauth2-proxy` in a forward-authentication setup (e.g., using Nginx `auth_request` pointing to `/oauth2/auth`).
2. Send an HTTP request to a protected endpoint (e.g., `/admin`) with either:
a. The `User-Agent` header set to a configured health check User-Agent (e.g., `GoogleHC/1.0` if GCP health checks are enabled).
b. No `User-Agent` header at all (if `ping-user-agent` is left at its default empty value).
3. The health check middleware will match the `User-Agent` and immediately return `200 OK`.
4. Nginx will interpret the `200 OK` response as successful authentication and forward the request to the upstream service, bypassing authentication entirely.
```bash
# Bypass authentication when GCPHealthChecks is enabled
curl -H "User-Agent: GoogleHC/1.0" https://protected-app.example.com/admin
# Bypass authentication by default (when no PingUserAgent is configured, matching the empty string)
curl -H "User-Agent:" https://protected-app.example.com/admin
```
Affected Code:
func isHealthCheckRequest(paths, userAgents map[string]struct{}, req *http.Request) bool {
if _, ok := paths[req.URL.EscapedPath()]; ok {
return true
}
if _, ok := userAgents[req.Header.Get("User-Agent")]; ok {
return true
}
return false
}
Acceptance criteria:
- Acceptance is defined by the **actual reported behavior**, not by tests passing.
- Reproduce the issue, or narrow the exact code path that produces it, *before* changing code. State what you confirmed.
- Fix the underlying cause. Mitigations that paper over the reported behavior do not count as a fix.
- Add a regression test that fails on the unpatched code and passes on the fix. If a regression test is genuinely impractical (e.g. race condition, infra-level issue), say so and explain why.
- Existing tests passing is **not** the bar. Do not declare done on tests-pass theatre.
Only change what is necessary to fix this vulnerability. Do not refactor adjacent code or modify unrelated files.
Triage: Reply !fp <reason> (false positive), !valid (confirmed), !accepted_risk <reason>, or !fixed (resolved). Any other reply is saved as a triage note.
Reason is optional but improves future scans — e.g. !fp internal endpoint, not user-facing.
Fresh PR to trigger a full-scope Hacktron PR scan (opened event) after rebuilding the local sandbox image. Same vulnerable diff as #2/#3 (base
base/pr-620, head at pre-fix commit4727bcf4).