Moderate severityNVD Advisory· Published Aug 1, 2024· Updated Aug 2, 2024
Malicious remote can make an arbitrary local channel read-only
CVE-2024-41162
Description
Mattermost versions 9.9.x <= 9.9.0, 9.5.x <= 9.5.6, 9.7.x <= 9.7.5 and 9.8.x <= 9.8.1 fail to disallow the modification of local channels by a remote, when shared channels are enabled, which allows a malicious remote to make an arbitrary local channel read-only.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/mattermost/mattermost/server/v8Go | >= 9.5.0, < 9.5.7 | 9.5.7 |
github.com/mattermost/mattermost/server/v8Go | >= 9.7.0, < 9.7.6 | 9.7.6 |
github.com/mattermost/mattermost/server/v8Go | >= 9.8.0, < 9.8.2 | 9.8.2 |
github.com/mattermost/mattermost/server/v8Go | >= 9.9.0, < 9.9.1 | 9.9.1 |
github.com/mattermost/mattermost/server/v8Go | < 8.0.0-20240628125750-70b218839fa7 | 8.0.0-20240628125750-70b218839fa7 |
github.com/mattermost/mattermostGo | < 5.3.2-0.20240628125750-70b218839fa7 | 5.3.2-0.20240628125750-70b218839fa7 |
Affected products
1- Range: 9.9.0
Patches
333db68643e85MM-58255 Ensure remote users do not get valid email addresses (#27421) (#27454)
7 files changed · +41 −15
server/channels/api4/user.go+6 −0 modified@@ -2415,6 +2415,12 @@ func createUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { audit.AddEventParameterAuditable(auditRec, "user", user) + if user.IsRemote() { + // remote/synthetic users cannot have access tokens + c.SetPermissionError(model.PermissionCreateUserAccessToken) + return + } + if c.AppContext.Session().IsOAuth { c.SetPermissionError(model.PermissionCreateUserAccessToken) c.Err.DetailedError += ", attempted access by oauth app"
server/channels/api4/user_test.go+21 −0 modified@@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" "github.com/mattermost/mattermost/server/v8/channels/utils/testutils" "github.com/mattermost/mattermost/server/v8/einterfaces/mocks" @@ -4634,6 +4635,26 @@ func TestCreateUserAccessToken(t *testing.T) { assertToken(t, th, rtoken, th.BasicUser.Id) }) + t.Run("create user access token for remote user as a system admin", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + // make a remote user + remoteUser, appErr := th.App.CreateUser(request.TestContext(t), &model.User{ + Username: "remoteuser", + RemoteId: model.NewString(model.NewId()), + Password: model.NewId(), + Email: "remoteuser@example.com", + }) + require.Nil(t, appErr) + + _, resp, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), remoteUser.Id, "test token") + require.Error(t, err) + CheckForbiddenStatus(t, resp) // remote users are not allowed to have access tokens + }) + t.Run("create access token as oauth session", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown()
server/channels/app/notification.go+2 −2 modified@@ -1102,8 +1102,8 @@ func max(a, b int64) int64 { } func (a *App) userAllowsEmail(c request.CTX, user *model.User, channelMemberNotificationProps model.StringMap, post *model.Post) bool { - // if user is a bot account, then we do not send email - if user.IsBot { + // if user is a bot account or remote, then we do not send email + if user.IsBot || user.IsRemote() { return false }
server/platform/services/sharedchannel/sync_recv.go+2 −2 modified@@ -269,7 +269,7 @@ func (scs *Service) insertSyncUser(rctx request.CTX, user *model.User, _ *model. } user.Username = mungUsername(user.Username, rc.Name, suffix, model.UserNameMaxLength) - user.Email = mungEmail(rc.Name, model.UserEmailMaxLength) + user.Email = model.NewId() if userSaved, err = scs.server.GetStore().User().Save(rctx, user); err != nil { field, ok := isConflictError(err) @@ -323,7 +323,7 @@ func (scs *Service) updateSyncUser(rctx request.CTX, patch *model.UserPatch, use suffix = strconv.FormatInt(int64(i), 10) } user.Username = mungUsername(user.Username, rc.Name, suffix, model.UserNameMaxLength) - user.Email = mungEmail(rc.Name, model.UserEmailMaxLength) + user.Email = model.NewId() if update, err = scs.server.GetStore().User().Update(rctx, user, false); err != nil { field, ok := isConflictError(err)
server/platform/services/sharedchannel/util.go+0 −9 modified@@ -95,15 +95,6 @@ func mungUsername(username string, remotename string, suffix string, maxLen int) return fmt.Sprintf("%s%s%s:%s%s", username, suffix, userEllipses, remotename, remoteEllipses) } -// mungEmail creates a unique email address using a UID and remote name. -func mungEmail(remotename string, maxLen int) string { - s := fmt.Sprintf("%s@%s", model.NewId(), remotename) - if len(s) > maxLen { - s = s[:maxLen] - } - return s -} - func isConflictError(err error) (string, bool) { if err == nil { return "", false
server/public/model/user.go+1 −1 modified@@ -346,7 +346,7 @@ func (u *User) IsValid() *AppError { } } - if len(u.Email) > UserEmailMaxLength || u.Email == "" || !IsValidEmail(u.Email) { + if len(u.Email) > UserEmailMaxLength || u.Email == "" || (!IsValidEmail(u.Email) && !u.IsRemote()) { return InvalidUserError("email", u.Id, u.Email) }
server/public/model/user_test.go+9 −1 modified@@ -9,9 +9,10 @@ import ( "strings" "testing" - "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/shared/mlog" ) func TestUserDeepCopy(t *testing.T) { @@ -117,6 +118,13 @@ func TestUserIsValid(t *testing.T) { user.LastName = "" require.Nil(t, user.IsValid()) + user.Email = NewId() + appErr = user.IsValid() + require.True(t, HasExpectedUserIsValidError(appErr, "email", user.Id, user.Email), "expected user is valid error: %s", appErr.Error()) + + user.RemoteId = NewString(NewId()) + require.Nil(t, user.IsValid()) + user.FirstName = strings.Repeat("a", 65) appErr = user.IsValid() require.True(t, HasExpectedUserIsValidError(appErr, "first_name", user.Id, user.FirstName), "expected user is valid error: %s", appErr.Error())
55b24d27c857MM-58255 Ensure remote users do not get valid email addresses (#27421) (#27456)
7 files changed · +41 −15
server/channels/api4/user.go+6 −0 modified@@ -2414,6 +2414,12 @@ func createUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { audit.AddEventParameterAuditable(auditRec, "user", user) + if user.IsRemote() { + // remote/synthetic users cannot have access tokens + c.SetPermissionError(model.PermissionCreateUserAccessToken) + return + } + if c.AppContext.Session().IsOAuth { c.SetPermissionError(model.PermissionCreateUserAccessToken) c.Err.DetailedError += ", attempted access by oauth app"
server/channels/api4/user_test.go+21 −0 modified@@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" "github.com/mattermost/mattermost/server/v8/channels/utils/testutils" "github.com/mattermost/mattermost/server/v8/einterfaces/mocks" @@ -4634,6 +4635,26 @@ func TestCreateUserAccessToken(t *testing.T) { assertToken(t, th, rtoken, th.BasicUser.Id) }) + t.Run("create user access token for remote user as a system admin", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + // make a remote user + remoteUser, appErr := th.App.CreateUser(request.TestContext(t), &model.User{ + Username: "remoteuser", + RemoteId: model.NewString(model.NewId()), + Password: model.NewId(), + Email: "remoteuser@example.com", + }) + require.Nil(t, appErr) + + _, resp, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), remoteUser.Id, "test token") + require.Error(t, err) + CheckForbiddenStatus(t, resp) // remote users are not allowed to have access tokens + }) + t.Run("create access token as oauth session", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown()
server/channels/app/notification.go+2 −2 modified@@ -832,8 +832,8 @@ func max(a, b int64) int64 { } func (a *App) userAllowsEmail(c request.CTX, user *model.User, channelMemberNotificationProps model.StringMap, post *model.Post) bool { - // if user is a bot account, then we do not send email - if user.IsBot { + // if user is a bot account or remote, then we do not send email + if user.IsBot || user.IsRemote() { return false }
server/platform/services/sharedchannel/sync_recv.go+2 −2 modified@@ -267,7 +267,7 @@ func (scs *Service) insertSyncUser(user *model.User, channel *model.Channel, rc } user.Username = mungUsername(user.Username, rc.Name, suffix, model.UserNameMaxLength) - user.Email = mungEmail(rc.Name, model.UserEmailMaxLength) + user.Email = model.NewId() if userSaved, err = scs.server.GetStore().User().Save(user); err != nil { field, ok := isConflictError(err) @@ -321,7 +321,7 @@ func (scs *Service) updateSyncUser(rctx request.CTX, patch *model.UserPatch, use suffix = strconv.FormatInt(int64(i), 10) } user.Username = mungUsername(user.Username, rc.Name, suffix, model.UserNameMaxLength) - user.Email = mungEmail(rc.Name, model.UserEmailMaxLength) + user.Email = model.NewId() if update, err = scs.server.GetStore().User().Update(rctx, user, false); err != nil { field, ok := isConflictError(err)
server/platform/services/sharedchannel/util.go+0 −9 modified@@ -95,15 +95,6 @@ func mungUsername(username string, remotename string, suffix string, maxLen int) return fmt.Sprintf("%s%s%s:%s%s", username, suffix, userEllipses, remotename, remoteEllipses) } -// mungEmail creates a unique email address using a UID and remote name. -func mungEmail(remotename string, maxLen int) string { - s := fmt.Sprintf("%s@%s", model.NewId(), remotename) - if len(s) > maxLen { - s = s[:maxLen] - } - return s -} - func isConflictError(err error) (string, bool) { if err == nil { return "", false
server/public/model/user.go+1 −1 modified@@ -346,7 +346,7 @@ func (u *User) IsValid() *AppError { } } - if len(u.Email) > UserEmailMaxLength || u.Email == "" || !IsValidEmail(u.Email) { + if len(u.Email) > UserEmailMaxLength || u.Email == "" || (!IsValidEmail(u.Email) && !u.IsRemote()) { return InvalidUserError("email", u.Id, u.Email) }
server/public/model/user_test.go+9 −1 modified@@ -9,9 +9,10 @@ import ( "strings" "testing" - "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/shared/mlog" ) func TestUserDeepCopy(t *testing.T) { @@ -117,6 +118,13 @@ func TestUserIsValid(t *testing.T) { user.LastName = "" require.Nil(t, user.IsValid()) + user.Email = NewId() + appErr = user.IsValid() + require.True(t, HasExpectedUserIsValidError(appErr, "email", user.Id, user.Email), "expected user is valid error: %s", appErr.Error()) + + user.RemoteId = NewString(NewId()) + require.Nil(t, user.IsValid()) + user.FirstName = strings.Repeat("a", 65) appErr = user.IsValid() require.True(t, HasExpectedUserIsValidError(appErr, "first_name", user.Id, user.FirstName), "expected user is valid error: %s", appErr.Error())
d45148a45866MM-58255 Ensure remote users do not get valid email addresses (#27421) (#27455)
7 files changed · +41 −15
server/channels/api4/user.go+6 −0 modified@@ -2435,6 +2435,12 @@ func createUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { audit.AddEventParameterAuditable(auditRec, "user", user) + if user.IsRemote() { + // remote/synthetic users cannot have access tokens + c.SetPermissionError(model.PermissionCreateUserAccessToken) + return + } + if c.AppContext.Session().IsOAuth { c.SetPermissionError(model.PermissionCreateUserAccessToken) c.Err.DetailedError += ", attempted access by oauth app"
server/channels/api4/user_test.go+21 −0 modified@@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/require" "github.com/mattermost/mattermost/server/public/model" + "github.com/mattermost/mattermost/server/public/shared/request" "github.com/mattermost/mattermost/server/v8/channels/app" "github.com/mattermost/mattermost/server/v8/channels/utils/testutils" "github.com/mattermost/mattermost/server/v8/einterfaces/mocks" @@ -4634,6 +4635,26 @@ func TestCreateUserAccessToken(t *testing.T) { assertToken(t, th, rtoken, th.BasicUser.Id) }) + t.Run("create user access token for remote user as a system admin", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true }) + + // make a remote user + remoteUser, appErr := th.App.CreateUser(request.TestContext(t), &model.User{ + Username: "remoteuser", + RemoteId: model.NewString(model.NewId()), + Password: model.NewId(), + Email: "remoteuser@example.com", + }) + require.Nil(t, appErr) + + _, resp, err := th.SystemAdminClient.CreateUserAccessToken(context.Background(), remoteUser.Id, "test token") + require.Error(t, err) + CheckForbiddenStatus(t, resp) // remote users are not allowed to have access tokens + }) + t.Run("create access token as oauth session", func(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown()
server/channels/app/notification.go+2 −2 modified@@ -1065,8 +1065,8 @@ func max(a, b int64) int64 { } func (a *App) userAllowsEmail(c request.CTX, user *model.User, channelMemberNotificationProps model.StringMap, post *model.Post) bool { - // if user is a bot account, then we do not send email - if user.IsBot { + // if user is a bot account or remote, then we do not send email + if user.IsBot || user.IsRemote() { return false }
server/platform/services/sharedchannel/sync_recv.go+2 −2 modified@@ -269,7 +269,7 @@ func (scs *Service) insertSyncUser(rctx request.CTX, user *model.User, _ *model. } user.Username = mungUsername(user.Username, rc.Name, suffix, model.UserNameMaxLength) - user.Email = mungEmail(rc.Name, model.UserEmailMaxLength) + user.Email = model.NewId() if userSaved, err = scs.server.GetStore().User().Save(rctx, user); err != nil { field, ok := isConflictError(err) @@ -323,7 +323,7 @@ func (scs *Service) updateSyncUser(rctx request.CTX, patch *model.UserPatch, use suffix = strconv.FormatInt(int64(i), 10) } user.Username = mungUsername(user.Username, rc.Name, suffix, model.UserNameMaxLength) - user.Email = mungEmail(rc.Name, model.UserEmailMaxLength) + user.Email = model.NewId() if update, err = scs.server.GetStore().User().Update(rctx, user, false); err != nil { field, ok := isConflictError(err)
server/platform/services/sharedchannel/util.go+0 −9 modified@@ -95,15 +95,6 @@ func mungUsername(username string, remotename string, suffix string, maxLen int) return fmt.Sprintf("%s%s%s:%s%s", username, suffix, userEllipses, remotename, remoteEllipses) } -// mungEmail creates a unique email address using a UID and remote name. -func mungEmail(remotename string, maxLen int) string { - s := fmt.Sprintf("%s@%s", model.NewId(), remotename) - if len(s) > maxLen { - s = s[:maxLen] - } - return s -} - func isConflictError(err error) (string, bool) { if err == nil { return "", false
server/public/model/user.go+1 −1 modified@@ -346,7 +346,7 @@ func (u *User) IsValid() *AppError { } } - if len(u.Email) > UserEmailMaxLength || u.Email == "" || !IsValidEmail(u.Email) { + if len(u.Email) > UserEmailMaxLength || u.Email == "" || (!IsValidEmail(u.Email) && !u.IsRemote()) { return InvalidUserError("email", u.Id, u.Email) }
server/public/model/user_test.go+9 −1 modified@@ -9,9 +9,10 @@ import ( "strings" "testing" - "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/mattermost/mattermost/server/public/shared/mlog" ) func TestUserDeepCopy(t *testing.T) { @@ -117,6 +118,13 @@ func TestUserIsValid(t *testing.T) { user.LastName = "" require.Nil(t, user.IsValid()) + user.Email = NewId() + appErr = user.IsValid() + require.True(t, HasExpectedUserIsValidError(appErr, "email", user.Id, user.Email), "expected user is valid error: %s", appErr.Error()) + + user.RemoteId = NewString(NewId()) + require.Nil(t, user.IsValid()) + user.FirstName = strings.Repeat("a", 65) appErr = user.IsValid() require.True(t, HasExpectedUserIsValidError(appErr, "first_name", user.Id, user.FirstName), "expected user is valid error: %s", appErr.Error())
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
4- github.com/advisories/GHSA-jr9x-3x7m-4j75ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-41162ghsaADVISORY
- mattermost.com/security-updatesghsaWEB
- pkg.go.dev/vuln/GO-2024-3031ghsaWEB
News mentions
0No linked articles in our index yet.