High severityNVD Advisory· Published Sep 15, 2025· Updated Sep 15, 2025
One-Click Mattermost Account Takeover via Poisoned RelayState SAML Parameter
CVE-2025-9072
Description
Mattermost versions 10.10.x <= 10.10.1, 10.5.x <= 10.5.9, 10.9.x <= 10.9.4 fail to validate the redirect_to parameter, allowing an attacker to craft a malicious link that, once a user authenticates with their SAML provider, could post the user’s cookies to an attacker-controlled URL.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/mattermost/mattermost-serverGo | >= 10.10.0, < 10.10.2 | 10.10.2 |
github.com/mattermost/mattermost-serverGo | >= 10.5.0, < 10.5.10 | 10.5.10 |
github.com/mattermost/mattermost-serverGo | >= 10.9.0, < 10.9.5 | 10.9.5 |
github.com/mattermost/mattermost/server/v8Go | < 8.0.0-20250731063404-9eebaadf8f72 | 8.0.0-20250731063404-9eebaadf8f72 |
Affected products
1- Range: 10.10.0
Patches
3fda403fb6ec4[MM-64911] Ensure redirect URL is validated before redirecting (#33559) (#33600)
2 files changed · +58 −8
server/channels/web/oauth.go+39 −3 modified@@ -9,6 +9,7 @@ import ( "html" "net/http" "net/url" + "path" "path/filepath" "strings" "time" @@ -536,13 +537,48 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } func fullyQualifiedRedirectURL(siteURLPrefix, targetURL string) string { - parsed, _ := url.Parse(targetURL) - if parsed == nil || parsed.Scheme != "" || parsed.Host != "" { + parsed, err := url.Parse(targetURL) + if err != nil { + return siteURLPrefix + } + prefixParsed, err := url.Parse(siteURLPrefix) + if err != nil { + return siteURLPrefix + } + + // Check if the targetURL is a valid URL and is within the siteURLPrefix + sameScheme := parsed.Scheme == prefixParsed.Scheme + sameHost := parsed.Host == prefixParsed.Host + safePath := strings.HasPrefix(path.Clean(parsed.Path), path.Clean(prefixParsed.Path)) + + if sameScheme && sameHost && safePath { return targetURL + } else if parsed.Scheme != "" || parsed.Host != "" { + return siteURLPrefix } + // For relative URLs, normalize and join with siteURLPrefix if targetURL != "" && targetURL[0] != '/' { targetURL = "/" + targetURL } - return siteURLPrefix + targetURL + + // Check for path traversal + joinedURL, err := url.JoinPath(siteURLPrefix, targetURL) + if err != nil { + return siteURLPrefix + } + unescapedURL, err := url.PathUnescape(joinedURL) + if err != nil { + return siteURLPrefix + } + parsed, err = url.Parse(unescapedURL) + if err != nil { + return siteURLPrefix + } + + if !strings.HasPrefix(path.Clean(parsed.Path), path.Clean(prefixParsed.Path)) { + return siteURLPrefix + } + + return parsed.String() }
server/channels/web/oauth_test.go+19 −5 modified@@ -850,11 +850,25 @@ 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{ - "": "https://xxx.yyy/mm", - "/": "https://xxx.yyy/mm/", - "some-path": "https://xxx.yyy/mm/some-path", - "/some-path": "https://xxx.yyy/mm/some-path", - "/some-path/": "https://xxx.yyy/mm/some-path/", + "": siteURL, + "/": siteURL + "/", + "some-path": siteURL + "/some-path", + "/some-path": siteURL + "/some-path", + "/some-path/": siteURL + "/some-path/", + "/some-path?foo=bar": siteURL + "/some-path?foo=bar", + "/some-path#section": siteURL + "/some-path#section", + "../bad-path": siteURL, + "/index.html": siteURL + "/index.html", + "//evil.com": siteURL, + "https://xxx.yyy/mm": siteURL, + "https://xxx.yyy/mm//double-concat": siteURL + "//double-concat", + "https://xxx.yyy/other-path/": siteURL, + "https://xxx.yyy/mm/some-path": siteURL + "/some-path", + "https://yyy.zzz/mm/some-path": siteURL, + "https://xxx.yyy/mm/some-path?foo=bar": siteURL + "/some-path?foo=bar", + "https://xxx.yyy/mm/some-path#section": siteURL + "/some-path#section", + "https://xxx.yyy/mm/../malicious-path": siteURL, + ":foo": siteURL, } { t.Run(target, func(t *testing.T) { require.Equal(t, expected, fullyQualifiedRedirectURL(siteURL, target))
13cd76009d31[MM-64911] Ensure redirect URL is validated before redirecting (#33559) (#33598)
2 files changed · +58 −8
server/channels/web/oauth.go+39 −3 modified@@ -9,6 +9,7 @@ import ( "html" "net/http" "net/url" + "path" "path/filepath" "strings" "time" @@ -536,13 +537,48 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } func fullyQualifiedRedirectURL(siteURLPrefix, targetURL string) string { - parsed, _ := url.Parse(targetURL) - if parsed == nil || parsed.Scheme != "" || parsed.Host != "" { + parsed, err := url.Parse(targetURL) + if err != nil { + return siteURLPrefix + } + prefixParsed, err := url.Parse(siteURLPrefix) + if err != nil { + return siteURLPrefix + } + + // Check if the targetURL is a valid URL and is within the siteURLPrefix + sameScheme := parsed.Scheme == prefixParsed.Scheme + sameHost := parsed.Host == prefixParsed.Host + safePath := strings.HasPrefix(path.Clean(parsed.Path), path.Clean(prefixParsed.Path)) + + if sameScheme && sameHost && safePath { return targetURL + } else if parsed.Scheme != "" || parsed.Host != "" { + return siteURLPrefix } + // For relative URLs, normalize and join with siteURLPrefix if targetURL != "" && targetURL[0] != '/' { targetURL = "/" + targetURL } - return siteURLPrefix + targetURL + + // Check for path traversal + joinedURL, err := url.JoinPath(siteURLPrefix, targetURL) + if err != nil { + return siteURLPrefix + } + unescapedURL, err := url.PathUnescape(joinedURL) + if err != nil { + return siteURLPrefix + } + parsed, err = url.Parse(unescapedURL) + if err != nil { + return siteURLPrefix + } + + if !strings.HasPrefix(path.Clean(parsed.Path), path.Clean(prefixParsed.Path)) { + return siteURLPrefix + } + + return parsed.String() }
server/channels/web/oauth_test.go+19 −5 modified@@ -862,11 +862,25 @@ 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{ - "": "https://xxx.yyy/mm", - "/": "https://xxx.yyy/mm/", - "some-path": "https://xxx.yyy/mm/some-path", - "/some-path": "https://xxx.yyy/mm/some-path", - "/some-path/": "https://xxx.yyy/mm/some-path/", + "": siteURL, + "/": siteURL + "/", + "some-path": siteURL + "/some-path", + "/some-path": siteURL + "/some-path", + "/some-path/": siteURL + "/some-path/", + "/some-path?foo=bar": siteURL + "/some-path?foo=bar", + "/some-path#section": siteURL + "/some-path#section", + "../bad-path": siteURL, + "/index.html": siteURL + "/index.html", + "//evil.com": siteURL, + "https://xxx.yyy/mm": siteURL, + "https://xxx.yyy/mm//double-concat": siteURL + "//double-concat", + "https://xxx.yyy/other-path/": siteURL, + "https://xxx.yyy/mm/some-path": siteURL + "/some-path", + "https://yyy.zzz/mm/some-path": siteURL, + "https://xxx.yyy/mm/some-path?foo=bar": siteURL + "/some-path?foo=bar", + "https://xxx.yyy/mm/some-path#section": siteURL + "/some-path#section", + "https://xxx.yyy/mm/../malicious-path": siteURL, + ":foo": siteURL, } { t.Run(target, func(t *testing.T) { require.Equal(t, expected, fullyQualifiedRedirectURL(siteURL, target))
9eebaadf8f72[MM-64911] Ensure redirect URL is validated before redirecting (#33559) (#33597)
2 files changed · +58 −8
server/channels/web/oauth.go+39 −3 modified@@ -9,6 +9,7 @@ import ( "html" "net/http" "net/url" + "path" "path/filepath" "strings" "time" @@ -536,13 +537,48 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) { } func fullyQualifiedRedirectURL(siteURLPrefix, targetURL string) string { - parsed, _ := url.Parse(targetURL) - if parsed == nil || parsed.Scheme != "" || parsed.Host != "" { + parsed, err := url.Parse(targetURL) + if err != nil { + return siteURLPrefix + } + prefixParsed, err := url.Parse(siteURLPrefix) + if err != nil { + return siteURLPrefix + } + + // Check if the targetURL is a valid URL and is within the siteURLPrefix + sameScheme := parsed.Scheme == prefixParsed.Scheme + sameHost := parsed.Host == prefixParsed.Host + safePath := strings.HasPrefix(path.Clean(parsed.Path), path.Clean(prefixParsed.Path)) + + if sameScheme && sameHost && safePath { return targetURL + } else if parsed.Scheme != "" || parsed.Host != "" { + return siteURLPrefix } + // For relative URLs, normalize and join with siteURLPrefix if targetURL != "" && targetURL[0] != '/' { targetURL = "/" + targetURL } - return siteURLPrefix + targetURL + + // Check for path traversal + joinedURL, err := url.JoinPath(siteURLPrefix, targetURL) + if err != nil { + return siteURLPrefix + } + unescapedURL, err := url.PathUnescape(joinedURL) + if err != nil { + return siteURLPrefix + } + parsed, err = url.Parse(unescapedURL) + if err != nil { + return siteURLPrefix + } + + if !strings.HasPrefix(path.Clean(parsed.Path), path.Clean(prefixParsed.Path)) { + return siteURLPrefix + } + + return parsed.String() }
server/channels/web/oauth_test.go+19 −5 modified@@ -862,11 +862,25 @@ 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{ - "": "https://xxx.yyy/mm", - "/": "https://xxx.yyy/mm/", - "some-path": "https://xxx.yyy/mm/some-path", - "/some-path": "https://xxx.yyy/mm/some-path", - "/some-path/": "https://xxx.yyy/mm/some-path/", + "": siteURL, + "/": siteURL + "/", + "some-path": siteURL + "/some-path", + "/some-path": siteURL + "/some-path", + "/some-path/": siteURL + "/some-path/", + "/some-path?foo=bar": siteURL + "/some-path?foo=bar", + "/some-path#section": siteURL + "/some-path#section", + "../bad-path": siteURL, + "/index.html": siteURL + "/index.html", + "//evil.com": siteURL, + "https://xxx.yyy/mm": siteURL, + "https://xxx.yyy/mm//double-concat": siteURL + "//double-concat", + "https://xxx.yyy/other-path/": siteURL, + "https://xxx.yyy/mm/some-path": siteURL + "/some-path", + "https://yyy.zzz/mm/some-path": siteURL, + "https://xxx.yyy/mm/some-path?foo=bar": siteURL + "/some-path?foo=bar", + "https://xxx.yyy/mm/some-path#section": siteURL + "/some-path#section", + "https://xxx.yyy/mm/../malicious-path": siteURL, + ":foo": siteURL, } { t.Run(target, func(t *testing.T) { require.Equal(t, expected, fullyQualifiedRedirectURL(siteURL, target))
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-69j8-prx2-vx98ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-9072ghsaADVISORY
- github.com/mattermost/mattermost/commit/13cd76009d31754db46115bddef5287a8a29871aghsaWEB
- github.com/mattermost/mattermost/commit/9eebaadf8f720788e99b6997337c8df330271326ghsaWEB
- github.com/mattermost/mattermost/commit/fda403fb6ec41bea8780bff198a26860f105e6e5ghsaWEB
- mattermost.com/security-updatesghsaWEB
News mentions
0No linked articles in our index yet.