diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 9297f3d062721..0a3f4b3b5b10e 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1648,23 +1648,24 @@ LEVEL = Info ;; - .livejournal.com ;; ;; Whether to allow signin in via OpenID -;ENABLE_OPENID_SIGNIN = true +;ENABLE_OPENID_SIGNIN = false ;; ;; Whether to allow registering via OpenID ;; Do not include to rely on rhw DISABLE_REGISTRATION setting ;;ENABLE_OPENID_SIGNUP = true ;; -;; Allowed URI patterns (POSIX regexp). -;; Space separated. -;; Only these would be allowed if non-blank. -;; Example value: trusted.domain.org trusted.domain.net +;; Allowed OpenID provider hosts (host matcher). +;; Space or comma separated. +;; Only these would be allowed if non-blank. Matches the host portion only. +;; Example value: trusted.domain.org *.trusted.domain.net ;WHITELISTED_URIS = ;; -;; Forbidden URI patterns (POSIX regexp). -;; Space separated. +;; Blocked OpenID provider hosts (host matcher). +;; Space or comma separated. ;; Only used if WHITELISTED_URIS is blank. -;; Example value: loadaverage.org/badguy stackexchange.com/.*spammer -;BLACKLISTED_URIS = +;; Built-in: loopback (localhost), private (LAN/intranet), external (public hosts), * (all hosts) +;; Default value blocks localhost, loopback, private, and IPv6 link-local to avoid SSRF. +;BLACKLISTED_URIS = localhost loopback private 169.254.0.0/16 fe80::/10 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/modules/setting/openid.go b/modules/setting/openid.go new file mode 100644 index 0000000000000..04f8c55f67761 --- /dev/null +++ b/modules/setting/openid.go @@ -0,0 +1,63 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "strings" + + "code.gitea.io/gitea/modules/hostmatcher" +) + +// OpenID settings +var OpenID = struct { + EnableSignIn bool + EnableSignUp bool + Allowlist *hostmatcher.HostMatchList + Blocklist *hostmatcher.HostMatchList +}{ + EnableSignIn: false, + EnableSignUp: false, + Allowlist: nil, + Blocklist: nil, +} + +var defaultOpenIDBlocklist = []string{ + "localhost", + hostmatcher.MatchBuiltinLoopback, + hostmatcher.MatchBuiltinPrivate, + "169.254.0.0/16", + "fe80::/10", +} + +func splitOpenIDHostList(raw string) []string { + return strings.FieldsFunc(raw, func(r rune) bool { + switch r { + case ',', ' ', '\t', '\n', '\r': + return true + default: + return false + } + }) +} + +// loadOpenIDSetting loads OpenID settings from rootCfg, depends on service settings, +// so should be called after loadServiceSettings. +func loadOpenIDSetting(rootCfg ConfigProvider) { + sec := rootCfg.Section("openid") + OpenID.EnableSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(false) + OpenID.EnableSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(!Service.DisableRegistration && OpenID.EnableSignIn) + OpenID.Allowlist = nil + OpenID.Blocklist = nil + allowlist := splitOpenIDHostList(sec.Key("WHITELISTED_URIS").String()) + if len(allowlist) != 0 { + OpenID.Allowlist = hostmatcher.ParseHostMatchList("openid.WHITELISTED_URIS", strings.Join(allowlist, ",")) + } + blocklist := splitOpenIDHostList(sec.Key("BLACKLISTED_URIS").String()) + if len(blocklist) == 0 { + blocklist = defaultOpenIDBlocklist + } + if len(blocklist) != 0 { + OpenID.Blocklist = hostmatcher.ParseHostMatchList("openid.BLACKLISTED_URIS", strings.Join(blocklist, ",")) + } +} diff --git a/modules/setting/openid_test.go b/modules/setting/openid_test.go new file mode 100644 index 0000000000000..7708412aabee5 --- /dev/null +++ b/modules/setting/openid_test.go @@ -0,0 +1,73 @@ +// Copyright 2026 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package setting + +import ( + "testing" + + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestLoadOpenIDSettings(t *testing.T) { + defer test.MockVariableValue(&Service)() + + t.Run("DefaultBlacklist", func(t *testing.T) { + cfg, err := NewConfigProviderFromData(` +[openid] +`) + assert.NoError(t, err) + loadServiceFrom(cfg) + + assert.False(t, OpenID.EnableSignIn) + assert.False(t, OpenID.EnableSignUp) + assert.Nil(t, OpenID.Allowlist) + if assert.NotNil(t, OpenID.Blocklist) { + assert.True(t, OpenID.Blocklist.MatchHostName("localhost")) + assert.True(t, OpenID.Blocklist.MatchHostName("127.0.0.1")) + assert.True(t, OpenID.Blocklist.MatchHostName("192.168.0.1")) + assert.True(t, OpenID.Blocklist.MatchHostName("fe80::1")) + assert.False(t, OpenID.Blocklist.MatchHostName("example.com")) + } + }) + + t.Run("AllowlistParsing", func(t *testing.T) { + cfg, err := NewConfigProviderFromData(` +[openid] +ENABLE_OPENID_SIGNIN = true +WHITELISTED_URIS = example.com, *.trusted.example +`) + assert.NoError(t, err) + loadServiceFrom(cfg) + + assert.True(t, OpenID.EnableSignIn) + assert.True(t, OpenID.EnableSignUp) + if assert.NotNil(t, OpenID.Allowlist) { + assert.True(t, OpenID.Allowlist.MatchHostName("example.com")) + assert.True(t, OpenID.Allowlist.MatchHostName("foo.trusted.example")) + assert.False(t, OpenID.Allowlist.MatchHostName("example.org")) + } + if assert.NotNil(t, OpenID.Blocklist) { + assert.True(t, OpenID.Blocklist.MatchHostName("localhost")) + } + }) + + t.Run("BlocklistParsing", func(t *testing.T) { + cfg, err := NewConfigProviderFromData(` +[openid] +BLACKLISTED_URIS = bad.example.com, 10.0.0.0/8 +`) + assert.NoError(t, err) + loadServiceFrom(cfg) + + assert.Nil(t, OpenID.Allowlist) + if assert.NotNil(t, OpenID.Blocklist) { + assert.True(t, OpenID.Blocklist.MatchHostName("bad.example.com")) + assert.True(t, OpenID.Blocklist.MatchHostName("10.1.1.1")) + assert.False(t, OpenID.Blocklist.MatchHostName("localhost")) + assert.False(t, OpenID.Blocklist.MatchHostName("good.example.com")) + } + }) +} diff --git a/modules/setting/service.go b/modules/setting/service.go index e652c13c9c9e3..1f5e00463c4a5 100644 --- a/modules/setting/service.go +++ b/modules/setting/service.go @@ -4,7 +4,6 @@ package setting import ( - "regexp" "runtime" "strings" "time" @@ -85,12 +84,6 @@ var Service = struct { UserDeleteWithCommentsMaxTime time.Duration ValidSiteURLSchemes []string - // OpenID settings - EnableOpenIDSignIn bool - EnableOpenIDSignUp bool - OpenIDWhitelist []*regexp.Regexp - OpenIDBlacklist []*regexp.Regexp - // Explore page settings Explore struct { RequireSigninView bool `ini:"REQUIRE_SIGNIN_VIEW"` @@ -265,26 +258,6 @@ func loadServiceFrom(rootCfg ConfigProvider) { loadQosSetting(rootCfg) } -func loadOpenIDSetting(rootCfg ConfigProvider) { - sec := rootCfg.Section("openid") - Service.EnableOpenIDSignIn = sec.Key("ENABLE_OPENID_SIGNIN").MustBool(!InstallLock) - Service.EnableOpenIDSignUp = sec.Key("ENABLE_OPENID_SIGNUP").MustBool(!Service.DisableRegistration && Service.EnableOpenIDSignIn) - pats := sec.Key("WHITELISTED_URIS").Strings(" ") - if len(pats) != 0 { - Service.OpenIDWhitelist = make([]*regexp.Regexp, len(pats)) - for i, p := range pats { - Service.OpenIDWhitelist[i] = regexp.MustCompilePOSIX(p) - } - } - pats = sec.Key("BLACKLISTED_URIS").Strings(" ") - if len(pats) != 0 { - Service.OpenIDBlacklist = make([]*regexp.Regexp, len(pats)) - for i, p := range pats { - Service.OpenIDBlacklist[i] = regexp.MustCompilePOSIX(p) - } - } -} - func loadQosSetting(rootCfg ConfigProvider) { sec := rootCfg.Section("qos") Service.QoS.Enabled = sec.Key("ENABLED").MustBool(false) diff --git a/modules/web/middleware/data.go b/modules/web/middleware/data.go index 41fb1e7e6f64d..6f679e6acd265 100644 --- a/modules/web/middleware/data.go +++ b/modules/web/middleware/data.go @@ -32,7 +32,7 @@ func CommonTemplateContextData() reqctx.ContextData { "DisableDownloadSourceArchives": setting.Repository.DisableDownloadSourceArchives, "EnableSwagger": setting.API.EnableSwagger, - "EnableOpenIDSignIn": setting.Service.EnableOpenIDSignIn, + "EnableOpenIDSignIn": setting.OpenID.EnableSignIn, "PageStartTime": time.Now(), "RunModeIsProd": setting.IsProd, diff --git a/routers/install/install.go b/routers/install/install.go index 81fcdfa384c58..42d3fcdf14b5c 100644 --- a/routers/install/install.go +++ b/routers/install/install.go @@ -113,8 +113,8 @@ func Install(ctx *context.Context) { form.RegisterConfirm = setting.Service.RegisterEmailConfirm form.MailNotify = setting.Service.EnableNotifyMail - form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn - form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp + form.EnableOpenIDSignIn = setting.OpenID.EnableSignIn + form.EnableOpenIDSignUp = setting.OpenID.EnableSignUp form.DisableRegistration = setting.Service.DisableRegistration form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration form.EnableCaptcha = setting.Service.EnableCaptcha diff --git a/routers/web/admin/config.go b/routers/web/admin/config.go index a449796ec1295..ff8cc168a5fc3 100644 --- a/routers/web/admin/config.go +++ b/routers/web/admin/config.go @@ -142,6 +142,7 @@ func Config(ctx *context.Context) { ctx.Data["LFS"] = setting.LFS ctx.Data["Service"] = setting.Service + ctx.Data["OpenID"] = setting.OpenID ctx.Data["DbCfg"] = setting.Database ctx.Data["Webhook"] = setting.Webhook ctx.Data["MailerEnabled"] = false diff --git a/routers/web/auth/openid.go b/routers/web/auth/openid.go index c9843146d455e..7c5fe3051337c 100644 --- a/routers/web/auth/openid.go +++ b/routers/web/auth/openid.go @@ -44,25 +44,30 @@ func SignInOpenID(ctx *context.Context) { ctx.HTML(http.StatusOK, tplSignInOpenID) } -// Check if the given OpenID URI is allowed by blacklist/whitelist +// Check if the given OpenID URI is allowed by blocklist/allowlist func allowedOpenIDURI(uri string) (err error) { + parsed, err := url.Parse(uri) + if err != nil { + return err + } + host := parsed.Hostname() + if host == "" { + return errors.New("invalid OpenID URI host") + } + // In case a Whitelist is present, URI must be in it // in order to be accepted - if len(setting.Service.OpenIDWhitelist) != 0 { - for _, pat := range setting.Service.OpenIDWhitelist { - if pat.MatchString(uri) { - return nil // pass - } + if allowList := setting.OpenID.Allowlist; allowList != nil && !allowList.IsEmpty() { + if allowList.MatchHostName(host) { + return nil // pass } // must match one of this or be refused - return errors.New("URI not allowed by whitelist") + return errors.New("URI not allowed by allowlist") } - // A blacklist match expliclty forbids - for _, pat := range setting.Service.OpenIDBlacklist { - if pat.MatchString(uri) { - return errors.New("URI forbidden by blacklist") - } + // A blocklist match expliclty forbids + if blockList := setting.OpenID.Blocklist; blockList != nil && blockList.MatchHostName(host) { + return errors.New("URI forbidden by blocklist") } return nil @@ -222,7 +227,7 @@ func signInOpenIDVerify(ctx *context.Context) { return } - if u != nil || !setting.Service.EnableOpenIDSignUp || setting.Service.AllowOnlyInternalRegistration { + if u != nil || !setting.OpenID.EnableSignUp || setting.Service.AllowOnlyInternalRegistration { ctx.Redirect(setting.AppSubURL + "/user/openid/connect") } else { ctx.Redirect(setting.AppSubURL + "/user/openid/register") @@ -239,7 +244,7 @@ func ConnectOpenID(ctx *context.Context) { ctx.Data["Title"] = "OpenID connect" ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsOpenIDConnect"] = true - ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp + ctx.Data["EnableOpenIDSignUp"] = setting.OpenID.EnableSignUp ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration ctx.Data["OpenID"] = oid userName, _ := ctx.Session.Get("openid_determined_username").(string) @@ -260,7 +265,7 @@ func ConnectOpenIDPost(ctx *context.Context) { ctx.Data["Title"] = "OpenID connect" ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsOpenIDConnect"] = true - ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp + ctx.Data["EnableOpenIDSignUp"] = setting.OpenID.EnableSignUp ctx.Data["OpenID"] = oid u, _, err := auth.UserSignIn(ctx, form.UserName, form.Password) @@ -297,7 +302,7 @@ func RegisterOpenID(ctx *context.Context) { ctx.Data["Title"] = "OpenID signup" ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsOpenIDRegister"] = true - ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp + ctx.Data["EnableOpenIDSignUp"] = setting.OpenID.EnableSignUp ctx.Data["AllowOnlyInternalRegistration"] = setting.Service.AllowOnlyInternalRegistration ctx.Data["EnableCaptcha"] = setting.Service.EnableCaptcha ctx.Data["Captcha"] = context.GetImageCaptcha() @@ -332,7 +337,7 @@ func RegisterOpenIDPost(ctx *context.Context) { ctx.Data["Title"] = "OpenID signup" ctx.Data["PageIsSignIn"] = true ctx.Data["PageIsOpenIDRegister"] = true - ctx.Data["EnableOpenIDSignUp"] = setting.Service.EnableOpenIDSignUp + ctx.Data["EnableOpenIDSignUp"] = setting.OpenID.EnableSignUp context.SetCaptchaData(ctx) ctx.Data["OpenID"] = oid diff --git a/routers/web/auth/openid_test.go b/routers/web/auth/openid_test.go new file mode 100644 index 0000000000000..8704dc7f0567a --- /dev/null +++ b/routers/web/auth/openid_test.go @@ -0,0 +1,44 @@ +// Copyright 2024 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package auth + +import ( + "testing" + + "code.gitea.io/gitea/modules/hostmatcher" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" +) + +func TestAllowedOpenIDURI(t *testing.T) { + defer test.MockVariableValue(&setting.Service)() + + t.Run("Whitelist", func(t *testing.T) { + setting.OpenID.Allowlist = hostmatcher.ParseHostMatchList("openid.WHITELISTED_URIS", "trusted.example.com,*.trusted.net") + setting.OpenID.Blocklist = hostmatcher.ParseHostMatchList("openid.BLACKLISTED_URIS", "trusted.example.com,bad.example.com") + + assert.NoError(t, allowedOpenIDURI("https://trusted.example.com/openid")) + assert.NoError(t, allowedOpenIDURI("https://sub.trusted.net")) + assert.Error(t, allowedOpenIDURI("https://trusted.example.com.evil.org")) + assert.Error(t, allowedOpenIDURI("https://bad.example.com")) + }) + + t.Run("Blacklist", func(t *testing.T) { + setting.OpenID.Allowlist = nil + setting.OpenID.Blocklist = hostmatcher.ParseHostMatchList("openid.BLACKLISTED_URIS", "bad.example.com,10.0.0.0/8") + + assert.Error(t, allowedOpenIDURI("https://bad.example.com")) + assert.Error(t, allowedOpenIDURI("https://10.1.1.1")) + assert.NoError(t, allowedOpenIDURI("https://good.example.com")) + }) + + t.Run("InvalidURI", func(t *testing.T) { + setting.OpenID.Allowlist = nil + setting.OpenID.Blocklist = nil + + assert.Error(t, allowedOpenIDURI("://bad")) + }) +} diff --git a/routers/web/web.go b/routers/web/web.go index d973064b22913..809801511824b 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -299,14 +299,14 @@ func registerWebRoutes(m *web.Router) { validation.AddBindingRules() openIDSignInEnabled := func(ctx *context.Context) { - if !setting.Service.EnableOpenIDSignIn { + if !setting.OpenID.EnableSignIn { ctx.HTTPError(http.StatusForbidden) return } } openIDSignUpEnabled := func(ctx *context.Context) { - if !setting.Service.EnableOpenIDSignUp { + if !setting.OpenID.EnableSignUp { ctx.HTTPError(http.StatusForbidden) return } diff --git a/services/auth/sspi.go b/services/auth/sspi.go index 64507539359c6..6b51977430836 100644 --- a/services/auth/sspi.go +++ b/services/auth/sspi.go @@ -84,7 +84,7 @@ func (s *SSPI) Verify(req *http.Request, w http.ResponseWriter, store DataStore, store.GetData()["Flash"] = map[string]string{ "ErrorMsg": err.Error(), } - store.GetData()["EnableOpenIDSignIn"] = setting.Service.EnableOpenIDSignIn + store.GetData()["EnableOpenIDSignIn"] = setting.OpenID.EnableSignIn store.GetData()["EnableSSPI"] = true // in this case, the Verify function is called in Gitea's web context // FIXME: it doesn't look good to render the page here, why not redirect? diff --git a/templates/admin/config.tmpl b/templates/admin/config.tmpl index 6dc6e0d5ea1eb..4fbff8bf54af2 100644 --- a/templates/admin/config.tmpl +++ b/templates/admin/config.tmpl @@ -138,9 +138,9 @@
{{ctx.Locale.Tr "admin.config.show_registration_button"}}
{{svg (Iif .Service.ShowRegistrationButton "octicon-check" "octicon-x")}}
{{ctx.Locale.Tr "admin.config.enable_openid_signup"}}
-
{{svg (Iif .Service.EnableOpenIDSignUp "octicon-check" "octicon-x")}}
+
{{svg (Iif .OpenID.EnableSignUp "octicon-check" "octicon-x")}}
{{ctx.Locale.Tr "admin.config.enable_openid_signin"}}
-
{{svg (Iif .Service.EnableOpenIDSignIn "octicon-check" "octicon-x")}}
+
{{svg (Iif .OpenID.EnableSignIn "octicon-check" "octicon-x")}}
{{ctx.Locale.Tr "admin.config.require_sign_in_view"}}
{{svg (Iif .Service.RequireSignInViewStrict "octicon-check" "octicon-x")}}
{{ctx.Locale.Tr "admin.config.mail_notify"}}