feat(oauth2): add dashboard_host setting for callback URL

Decouple OAuth2 callback host from agent install host. When dashboard_host
is empty, the request Host is passed through; otherwise non-reserved hosts
are pinned to dashboard_host. Added to reserved-host allowlist for NAT.
This commit is contained in:
naiba
2026-06-06 05:03:12 +00:00
parent c595728609
commit 66de244c2e
6 changed files with 42 additions and 22 deletions
+27 -15
View File
@@ -113,16 +113,18 @@ func TestOAuth2_VerifyState_HappyPath(t *testing.T) {
}
// GHSA-9rc6-8cjv-rcvx: getRedirectURL must not echo an attacker-controlled
// Host header into the OAuth2 callback URL. It may only trust a Host that the
// operator declared (InstallHost / ListenHost / ReservedHosts); any other Host
// must fall back to the configured InstallHost so a forged value cannot divert
// the victim's authorization code.
// Host header into the OAuth2 callback URL. When DashboardHost is set, only a
// Host the operator declared (DashboardHost / InstallHost / ListenHost /
// ReservedHosts) is trusted and any other Host is pinned to DashboardHost. When
// DashboardHost is empty the operator has not pinned a dashboard origin, so the
// request Host is passed through.
func setRedirectHostConf(t *testing.T, installHost, reservedHosts string) {
func setRedirectHostConf(t *testing.T, dashboardHost, installHost, reservedHosts string) {
t.Helper()
prev := singleton.Conf
singleton.Conf = &singleton.ConfigClass{Config: &model.Config{
ConfigDashboard: model.ConfigDashboard{
DashboardHost: dashboardHost,
InstallHost: installHost,
ReservedHosts: reservedHosts,
},
@@ -130,28 +132,38 @@ func setRedirectHostConf(t *testing.T, installHost, reservedHosts string) {
t.Cleanup(func() { singleton.Conf = prev })
}
func TestGetRedirectURL_RejectsForgedHostFallsBackToInstallHost(t *testing.T) {
setRedirectHostConf(t, "panel.example.com", "")
func TestGetRedirectURL_RejectsForgedHostFallsBackToDashboardHost(t *testing.T) {
setRedirectHostConf(t, "panel.example.com", "", "")
c, _ := newOAuth2Ctx(t)
c.Request.Host = "evil.attacker.test"
got := getRedirectURL(c)
require.Equal(t, "http://panel.example.com/api/v1/oauth2/callback", got,
"a forged Host must be ignored in favour of the configured InstallHost")
"a forged Host must be ignored in favour of the configured DashboardHost")
}
func TestGetRedirectURL_TrustsInstallHost(t *testing.T) {
setRedirectHostConf(t, "panel.example.com", "")
func TestGetRedirectURL_EmptyDashboardHostPassesThroughRequestHost(t *testing.T) {
setRedirectHostConf(t, "", "agent.example.com", "")
c, _ := newOAuth2Ctx(t)
c.Request.Host = "panel.example.com"
got := getRedirectURL(c)
require.Equal(t, "http://panel.example.com/api/v1/oauth2/callback", got,
"the declared InstallHost must be trusted verbatim")
"when DashboardHost is empty the request Host must be passed through, decoupled from InstallHost")
}
func TestGetRedirectURL_TrustsDashboardHost(t *testing.T) {
setRedirectHostConf(t, "panel.example.com", "", "")
c, _ := newOAuth2Ctx(t)
c.Request.Host = "panel.example.com"
got := getRedirectURL(c)
require.Equal(t, "http://panel.example.com/api/v1/oauth2/callback", got,
"the declared DashboardHost must be trusted verbatim")
}
func TestGetRedirectURL_TrustsReservedHostForMultiDomain(t *testing.T) {
setRedirectHostConf(t, "panel.example.com", "alt.example.com,panel2.example.com")
setRedirectHostConf(t, "panel.example.com", "", "alt.example.com,panel2.example.com")
c, _ := newOAuth2Ctx(t)
c.Request.Host = "panel2.example.com"
@@ -161,7 +173,7 @@ func TestGetRedirectURL_TrustsReservedHostForMultiDomain(t *testing.T) {
}
func TestGetRedirectURL_HonoursForwardedProtoOnTrustedHost(t *testing.T) {
setRedirectHostConf(t, "panel.example.com", "")
setRedirectHostConf(t, "panel.example.com", "", "")
c, _ := newOAuth2Ctx(t)
c.Request.Host = "panel.example.com"
c.Request.Header.Set("X-Forwarded-Proto", "https")
@@ -172,14 +184,14 @@ func TestGetRedirectURL_HonoursForwardedProtoOnTrustedHost(t *testing.T) {
}
func TestGetRedirectURL_ForgedHostCannotForceHTTPSOrigin(t *testing.T) {
setRedirectHostConf(t, "panel.example.com", "")
setRedirectHostConf(t, "panel.example.com", "", "")
c, _ := newOAuth2Ctx(t)
c.Request.Host = "evil.attacker.test"
c.Request.Header.Set("X-Forwarded-Proto", "https")
got := getRedirectURL(c)
require.Equal(t, "https://panel.example.com/api/v1/oauth2/callback", got,
"even with an https hint the host must collapse to InstallHost, never the forged origin")
"even with an https hint the host must collapse to DashboardHost, never the forged origin")
}
func TestOAuth2_Unbind_UnknownProviderRejected(t *testing.T) {