VYPR
Moderate severityNVD Advisory· Published Feb 16, 2026· Updated Feb 17, 2026

User profile update exposes password hash and MFA secrets

CVE-2025-13821

Description

Mattermost versions 11.1.x <= 11.1.2, 10.11.x <= 10.11.9, 11.2.x <= 11.2.1 fail to sanitize sensitive data in WebSocket messages which allows authenticated users to exfiltrate password hashes and MFA secrets via profile nickname updates or email verification events. Mattermost Advisory ID: MMSA-2025-00560

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/mattermost/mattermost/server/v8Go
< 8.0.0-20251210191531-cd17b61de41b8.0.0-20251210191531-cd17b61de41b
github.com/mattermost/mattermost-serverGo
>= 11.1.0
github.com/mattermost/mattermost-serverGo
>= 10.11.0
github.com/mattermost/mattermost-serverGo
>= 11.2.0
github.com/mattermost/mattermost-serverGo
< 5.3.2-0.20251210191531-cd17b61de41b5.3.2-0.20251210191531-cd17b61de41b

Affected products

1

Patches

1
cd17b61de41b

MM-66757: Improve WebSocket user update events (#34600)

https://github.com/mattermost/mattermostJesse HallamDec 10, 2025via ghsa
3 files changed · +112 32
  • server/channels/api4/apitestlib.go+2 2 modified
    @@ -1083,9 +1083,9 @@ func GenerateTestID() string {
     func CheckUserSanitization(tb testing.TB, user *model.User) {
     	tb.Helper()
     
    -	require.Equal(tb, "", user.Password, "password wasn't blank")
    +	require.Empty(tb, user.Password, "password wasn't blank")
     	require.Empty(tb, user.AuthData, "auth data wasn't blank")
    -	require.Equal(tb, "", user.MfaSecret, "mfa secret wasn't blank")
    +	require.Empty(tb, user.MfaSecret, "mfa secret wasn't blank")
     }
     
     func CheckEtag(tb testing.TB, data any, resp *model.Response) {
    
  • server/channels/api4/user_test.go+103 25 modified
    @@ -8233,40 +8233,118 @@ func TestUserUpdateEvents(t *testing.T) {
     
     	client1 := th.CreateClient()
     	th.LoginBasicWithClient(t, client1)
    -	WebSocketClient := th.CreateConnectedWebSocketClientWithClient(t, client1)
    -	resp := <-WebSocketClient.ResponseChannel
    -	require.Equal(t, resp.Status, model.StatusOk)
    +	wsClient1 := th.CreateConnectedWebSocketClientWithClient(t, client1)
     
     	client2 := th.CreateClient()
     	th.LoginBasic2WithClient(t, client2)
    -	WebSocketClient2 := th.CreateConnectedWebSocketClientWithClient(t, client2)
    -	resp = <-WebSocketClient2.ResponseChannel
    -	require.Equal(t, resp.Status, model.StatusOk)
    +	wsClient2 := th.CreateConnectedWebSocketClientWithClient(t, client2)
     
    -	time.Sleep(1000 * time.Millisecond)
    -
    -	th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
    -		// trigger user update for onlineUser2
    -		th.BasicUser.Nickname = "something_else"
    -		ruser, _, err := client1.UpdateUser(context.Background(), th.BasicUser)
    -		require.NoError(t, err)
    -		CheckUserSanitization(t, ruser)
    -
    -		assertExpectedWebsocketEvent(t, WebSocketClient, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
    +	t.Run("nickname", func(t *testing.T) {
    +		assertUpdated := func(t *testing.T, event *model.WebSocketEvent, expectedNickname string) *model.User {
     			eventUser, ok := event.GetData()["user"].(*model.User)
     			require.True(t, ok, "expected user")
    -			// assert eventUser.Id is same as th.BasicUser.Id
    -			assert.Equal(t, eventUser.Id, th.BasicUser.Id)
    -			// assert eventUser.NotifyProps isn't empty
    -			require.NotEmpty(t, eventUser.NotifyProps, "user event for source user should not be sanitized")
    +			assert.Equal(t, th.BasicUser.Id, eventUser.Id)
    +			assert.Equal(t, expectedNickname, eventUser.Nickname)
    +
    +			// Some fields must always be sanitized
    +			CheckUserSanitization(t, eventUser)
    +
    +			return eventUser
    +		}
    +
    +		t.Run("update", func(t *testing.T) {
    +			th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
    +				newNickname := model.NewUsername()
    +				th.BasicUser.Nickname = newNickname
    +				_, _, err := client1.UpdateUser(context.Background(), th.BasicUser)
    +				require.NoError(t, err)
    +
    +				assertExpectedWebsocketEvent(t, wsClient1, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
    +					eventUser := assertUpdated(t, event, newNickname)
    +					assert.NotEmpty(t, eventUser.NotifyProps, "source user should keep notify_props")
    +				})
    +
    +				assertExpectedWebsocketEvent(t, wsClient2, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
    +					eventUser := assertUpdated(t, event, newNickname)
    +					assert.Empty(t, eventUser.NotifyProps, "non-source users should have sanitized notify_props")
    +				})
    +			})
    +		})
    +
    +		t.Run("patch", func(t *testing.T) {
    +			th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
    +				newNickname := model.NewUsername()
    +
    +				_, _, err := client1.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
    +					Nickname: &newNickname,
    +				})
    +				require.NoError(t, err)
    +
    +				assertExpectedWebsocketEvent(t, wsClient1, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
    +					eventUser := assertUpdated(t, event, newNickname)
    +					assert.NotEmpty(t, eventUser.NotifyProps, "source user should keep notify_props")
    +				})
    +
    +				assertExpectedWebsocketEvent(t, wsClient2, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
    +					eventUser := assertUpdated(t, event, newNickname)
    +					assert.Empty(t, eventUser.NotifyProps, "non-source users should have sanitized notify_props")
    +				})
    +			})
     		})
    -		assertExpectedWebsocketEvent(t, WebSocketClient2, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
    +	})
    +
    +	t.Run("username", func(t *testing.T) {
    +		assertUpdated := func(t *testing.T, event *model.WebSocketEvent, expectedUsername string) *model.User {
     			eventUser, ok := event.GetData()["user"].(*model.User)
     			require.True(t, ok, "expected user")
    -			// assert eventUser.Id is same as th.BasicUser.Id
    -			assert.Equal(t, eventUser.Id, th.BasicUser.Id)
    -			// assert eventUser.NotifyProps is an empty map
    -			require.Empty(t, eventUser.NotifyProps, "user event for non-source users should be sanitized")
    +			assert.Equal(t, th.BasicUser.Id, eventUser.Id)
    +			assert.Equal(t, expectedUsername, eventUser.Username)
    +
    +			// Some fields must always be sanitized
    +			CheckUserSanitization(t, eventUser)
    +
    +			return eventUser
    +		}
    +
    +		t.Run("update", func(t *testing.T) {
    +			th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
    +				newUsername := model.NewUsername()
    +
    +				th.BasicUser.Username = newUsername
    +				_, _, err := client1.UpdateUser(context.Background(), th.BasicUser)
    +				require.NoError(t, err)
    +
    +				assertExpectedWebsocketEvent(t, wsClient1, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
    +					eventUser := assertUpdated(t, event, newUsername)
    +					assert.NotEmpty(t, eventUser.NotifyProps, "source user should keep notify_props")
    +				})
    +
    +				assertExpectedWebsocketEvent(t, wsClient2, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
    +					eventUser := assertUpdated(t, event, newUsername)
    +					assert.Empty(t, eventUser.NotifyProps, "non-source users should have sanitized notify_props")
    +				})
    +			})
    +		})
    +
    +		t.Run("patch", func(t *testing.T) {
    +			th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
    +				newUsername := model.NewUsername()
    +
    +				_, _, err := client1.PatchUser(context.Background(), th.BasicUser.Id, &model.UserPatch{
    +					Username: &newUsername,
    +				})
    +				require.NoError(t, err)
    +
    +				assertExpectedWebsocketEvent(t, wsClient1, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
    +					eventUser := assertUpdated(t, event, newUsername)
    +					assert.NotEmpty(t, eventUser.NotifyProps, "source user should keep notify_props")
    +				})
    +
    +				assertExpectedWebsocketEvent(t, wsClient2, model.WebsocketEventUserUpdated, func(event *model.WebSocketEvent) {
    +					eventUser := assertUpdated(t, event, newUsername)
    +					assert.Empty(t, eventUser.NotifyProps, "non-source users should have sanitized notify_props")
    +				})
    +			})
     		})
     	})
     }
    
  • server/channels/app/user.go+7 5 modified
    @@ -1380,9 +1380,11 @@ func (a *App) sendUpdatedUserEvent(user *model.User) {
     	// First, creating a base copy to avoid race conditions
     	// from setting the binaryParamKey in userstore.Update.
     	user = user.DeepCopy()
    -	// declare admin and unsanitized copy of user
    +	// Create copies for different sanitization levels:
    +	// - adminCopyOfUser: moderately sanitized for admins
    +	// - sourceUserCopyOfUser: minimally sanitized (keeps NotifyProps) for event creator
     	adminCopyOfUser := user.DeepCopy()
    -	unsanitizedCopyOfUser := user.DeepCopy()
    +	sourceUserCopyOfUser := user.DeepCopy()
     
     	a.SanitizeProfile(adminCopyOfUser, true)
     	adminMessage := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", "", omitUsers, "")
    @@ -1396,9 +1398,9 @@ func (a *App) sendUpdatedUserEvent(user *model.User) {
     	message.GetBroadcast().ContainsSanitizedData = true
     	a.Publish(message)
     
    -	// send unsanitized user to event creator
    -	sourceUserMessage := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", unsanitizedCopyOfUser.Id, nil, "")
    -	sourceUserMessage.Add("user", unsanitizedCopyOfUser)
    +	sourceUserCopyOfUser.Sanitize(nil)
    +	sourceUserMessage := model.NewWebSocketEvent(model.WebsocketEventUserUpdated, "", "", sourceUserCopyOfUser.Id, nil, "")
    +	sourceUserMessage.Add("user", sourceUserCopyOfUser)
     	a.Publish(sourceUserMessage)
     }
     
    

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

News mentions

0

No linked articles in our index yet.