GitHub Security Advisory: GHSA-p64j-f4x9-wq66
Package: github.com/lin-snow/Ech0 (GO)
Severity: HIGH (CVSS 8.0)
CWE: CWE-601: URL Redirection to Untrusted Site ('Open Redirect'), CWE-1173: Improper Use of Validation Framework
Vulnerable versions: < 1.4.8-0[PII_REDACTED]0728-a7e8b8e84bd1
Patched in: 1.4.8-0[PII_REDACTED]0728-a7e8b8e84bd1

Summary: Ech0's OAuth redirect URI validation ignores path component, enables exchange-code theft

Details:
## Summary

`parseAndValidateClientRedirect` at `internal/service/auth/auth.go:448` validates OAuth client-redirect URIs by comparing only scheme and host against the admin-configured allowlist. Path, query, and fragment are ignored. The initiator at `/oauth/:provider/login` embeds the caller-supplied `redirect_uri` verbatim into the signed state JWT without any validation at login time. Alice submits a crafted `redirect_uri` whose host matches an allowed origin but whose path points to any page on that host. After the provider exchange, Ech0 redirects the victim to the attacker-chosen path with the one-time exchange code in the query string. If the chosen path leaks the URL via Referer, analytics, or an open redirect, the attacker trades the code at `POST /api/auth/exchange` for the victim's access and refresh tokens. RFC 6749 §3.1.2 requires exact redirect URI matching.

## Details

Validation at `internal/service/auth/auth.go:448`:

```go
matched := false
for _, item := range allowed {
    allowURL, parseErr := url.Parse(strings.TrimSpace(item))
    if parseErr != nil || allowURL == nil || allowURL.Host == "" {
        continue
    }
    if strings.EqualFold(redirectURL.Scheme, allowURL.Scheme) &&
       strings.EqualFold(redirectURL.Host, allowURL.Host) {
        matched = true
        break
    }
}
```

Scheme and host compared via `EqualFold`. Path, query, fragment all ignored. An allowlist entry of `https://myecho.example.com/dashboard` matches every `https://myecho.example.com/<anything>` the attacker sends.

Login flow at `internal/service/auth/auth.go:141` (`GetOAuthLoginURL`) and the handler at `internal/handler/auth/oauth.go:43`:

```go
redirectURI := ctx.Query("redirect_uri")
redirectURL, err := h.authService.GetOAuthLoginURL(provider, redirectURI)
// ...
ctx.Redirect(302, redirectURL)
```

No validation at login. The raw `redirect_uri` query parameter is passed to `GetOAuthLoginURL`, which encodes it into the signed state JWT alongside the provider name and nonce. The state JWT travels through the OAuth provider and returns on the callback.

At callback time, `parseAndValidateClientRedirect(oauthState.Redirect)` fires at `internal/service/auth/auth.go:372` and `:427` inside the callback handler chain. Scheme and host are the only gates on the attacker-chosen URI.

After validation, the server generates a one-time exchange code and redirects the browser to the attacker-chosen path:

```
302 Location: https://myecho.example.com/<attacker-path>?code=<one-time-exchange-code>
```

The code is valid at the public endpoint `POST /api/auth/exchange` for up to 60 seconds (single-use). An attacker who reads the code from the URL trades it for the victim's access token and refresh token.

## Proof of Concept

Default install with OAuth2 configured. Admin allows `https://myecho.example.com/dashboard` as the return URL; Alice sends a crafted login link whose redirect points elsewhere on the same host:

```python
import requests, urllib.parse, ba