VYPR
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.

PackageAffected versionsPatched versions
github.com/mattermost/mattermost-serverGo
>= 10.10.0, < 10.10.210.10.2
github.com/mattermost/mattermost-serverGo
>= 10.5.0, < 10.5.1010.5.10
github.com/mattermost/mattermost-serverGo
>= 10.9.0, < 10.9.510.9.5
github.com/mattermost/mattermost/server/v8Go
< 8.0.0-20250731063404-9eebaadf8f728.0.0-20250731063404-9eebaadf8f72

Affected products

1

Patches

3
fda403fb6ec4

[MM-64911] Ensure redirect URL is validated before redirecting (#33559) (#33600)

https://github.com/mattermost/mattermostMattermost BuildJul 31, 2025via ghsa
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)

https://github.com/mattermost/mattermostMattermost BuildJul 31, 2025via ghsa
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)

https://github.com/mattermost/mattermostMattermost BuildJul 31, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.