diff --git a/server/channels/web/oauth.go b/server/channels/web/oauth.go index ad3cab71db..82bdae2655 100644 --- a/server/channels/web/oauth.go +++ b/server/channels/web/oauth.go @@ -11,6 +11,7 @@ import ( "net/url" "path" "path/filepath" + "slices" "strings" "time" @@ -22,6 +23,10 @@ import ( "github.com/mattermost/mattermost/server/v8/channels/utils/fileutils" ) +const ( + callbackHost = "callback" +) + func (w *Web) InitOAuth() { // API version independent OAuth 2.0 as a service provider endpoints w.MainRouter.Handle("/oauth/authorize", w.APIHandlerTrustRequester(authorizeOAuthPage)).Methods(http.MethodGet) @@ -315,7 +320,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) { hasRedirectURL = redirectURL != "" } } - redirectURL = fullyQualifiedRedirectURL(c.GetSiteURLHeader(), redirectURL) + redirectURL = fullyQualifiedRedirectURL(c.GetSiteURLHeader(), redirectURL, c.App.Config().NativeAppSettings.AppCustomURLSchemes) renderError := func(err *model.AppError) { if isMobile && hasRedirectURL { @@ -535,7 +540,7 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, authURL, http.StatusFound) } -func fullyQualifiedRedirectURL(siteURLPrefix, targetURL string) string { +func fullyQualifiedRedirectURL(siteURLPrefix, targetURL string, otherValidSchemes []string) string { parsed, err := url.Parse(targetURL) if err != nil { return siteURLPrefix @@ -544,8 +549,15 @@ func fullyQualifiedRedirectURL(siteURLPrefix, targetURL string) string { if err != nil { return siteURLPrefix } - - // Check if the targetURL is a valid URL and is within the siteURLPrefix + // mobile access + if slices.Contains(otherValidSchemes, fmt.Sprintf("%v://", parsed.Scheme)) && + parsed.Host == callbackHost && + parsed.Path == "" && + parsed.RawQuery == "" && + parsed.Fragment == "" { + return targetURL + } + // Check if the targetURL is valid and within the siteURLPrefix, excluding native app schemes like mmauth:// sameScheme := parsed.Scheme == prefixParsed.Scheme sameHost := parsed.Host == prefixParsed.Host safePath := strings.HasPrefix(path.Clean(parsed.Path), path.Clean(prefixParsed.Path)) diff --git a/server/channels/web/oauth_test.go b/server/channels/web/oauth_test.go index 1f3a2d110a..af6eb32f2b 100644 --- a/server/channels/web/oauth_test.go +++ b/server/channels/web/oauth_test.go @@ -861,6 +861,7 @@ func (th *TestHelper) AddPermissionToRole(permission string, roleName string) { func TestFullyQualifiedRedirectURL(t *testing.T) { const siteURL = "https://xxx.yyy/mm" + for target, expected := range map[string]string{ "": siteURL, "/": siteURL + "/", @@ -881,9 +882,11 @@ func TestFullyQualifiedRedirectURL(t *testing.T) { "https://xxx.yyy/mm/some-path#section": siteURL + "/some-path#section", "https://xxx.yyy/mm/../malicious-path": siteURL, ":foo": siteURL, + "mmauth://callback": "mmauth://callback", + "mmauth://xxx.yyy/mm": siteURL, // invalid mobile URL (wrong host) } { t.Run(target, func(t *testing.T) { - require.Equal(t, expected, fullyQualifiedRedirectURL(siteURL, target)) + require.Equal(t, expected, fullyQualifiedRedirectURL(siteURL, target, []string{"mmauth://"})) }) } } diff --git a/server/channels/web/saml.go b/server/channels/web/saml.go index 13d8502f49..f6bdfb27f3 100644 --- a/server/channels/web/saml.go +++ b/server/channels/web/saml.go @@ -117,7 +117,7 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) { redirectURL = val hasRedirectURL = val != "" } - redirectURL = fullyQualifiedRedirectURL(c.GetSiteURLHeader(), redirectURL) + redirectURL = fullyQualifiedRedirectURL(c.GetSiteURLHeader(), redirectURL, c.App.Config().NativeAppSettings.AppCustomURLSchemes) handleError := func(err *model.AppError) { if isMobile && hasRedirectURL {