fix(oauth2): validate Host header against allowlist for callback URL

GHSA-9rc6-8cjv-rcvx: getRedirectURL derived the OAuth2 callback URL from the
raw Host header, so a forged Host (or a provider with loose redirect-URI
matching) could divert a victim's authorization code to an attacker origin and
bind their identity. Trust the request Host only when it is an operator-declared
dashboard host (IsReservedDashboardHost, same allowlist guarding NAT routing);
otherwise fall back to the configured InstallHost.
This commit is contained in:
naiba
2026-06-05 02:42:28 +00:00
parent 1927b0c6a4
commit fb5c37e983
2 changed files with 83 additions and 1 deletions
+13 -1
View File
@@ -19,13 +19,25 @@ import (
"github.com/nezhahq/nezha/service/singleton"
)
// GHSA-9rc6-8cjv-rcvx: the OAuth2 callback URL is sent to the identity
// provider and is where the authorization code lands. Deriving it from the
// raw Host header lets an attacker who can reach this handler with a forged
// Host (or a provider with loose redirect-URI matching) divert a victim's
// code to their own origin and bind the victim's identity. Only trust the
// request Host when it is an operator-declared dashboard host (the same
// allowlist that guards NAT routing); otherwise fall back to the configured
// InstallHost so a forged Host cannot steer the redirect.
func getRedirectURL(c *gin.Context) string {
scheme := "http://"
referer := c.Request.Referer()
if forwardedProto := c.Request.Header.Get("X-Forwarded-Proto"); forwardedProto == "https" || strings.HasPrefix(referer, "https://") {
scheme = "https://"
}
return scheme + c.Request.Host + "/api/v1/oauth2/callback"
host := c.Request.Host
if !singleton.IsReservedDashboardHost(host) && singleton.Conf != nil && singleton.Conf.InstallHost != "" {
host = singleton.Conf.InstallHost
}
return scheme + host + "/api/v1/oauth2/callback"
}
// @Summary Get Oauth2 Redirect URL