VYPR
Moderate severityNVD Advisory· Published Nov 14, 2025· Updated Nov 14, 2025

Lack of MFA enforcement in WebSocket connections

CVE-2025-55070

Description

Mattermost versions <11 fail to enforce multi-factor authentication on WebSocket connections which allows unauthenticated users to access sensitive information via WebSocket events

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/mattermost/mattermost-serverGo
< 11.1.011.1.0
github.com/mattermost/mattermost/server/v8Go
< 8.0.0-20250912063506-7d8b7b5e4a608.0.0-20250912063506-7d8b7b5e4a60

Affected products

1

Patches

1
7d8b7b5e4a60

MM-63930: Lack of MFA enforcement in Websocket connections (#33381)

https://github.com/mattermost/mattermostcatalintomaiSep 12, 2025via ghsa
6 files changed · +196 5
  • server/channels/api4/websocket_test.go+145 0 modified
    @@ -12,6 +12,7 @@ import (
     	"testing"
     	"time"
     
    +	"github.com/dgryski/dgoogauth"
     	"github.com/gorilla/websocket"
     	"github.com/stretchr/testify/require"
     
    @@ -536,3 +537,147 @@ func TestValidateDisconnectErrCode(t *testing.T) {
     		})
     	}
     }
    +
    +// Helper function to enable MFA enforcement in config
    +func enableMFAEnforcement(th *TestHelper) {
    +	th.App.UpdateConfig(func(cfg *model.Config) {
    +		*cfg.ServiceSettings.EnableMultifactorAuthentication = true
    +		*cfg.ServiceSettings.EnforceMultifactorAuthentication = true
    +	})
    +}
    +
    +// Helper function to set up MFA for a user
    +func setupUserWithMFA(t *testing.T, th *TestHelper, user *model.User) string {
    +	// Setup MFA properly - following authentication_test.go pattern
    +	secret, appErr := th.App.GenerateMfaSecret(user.Id)
    +	require.Nil(t, appErr)
    +	err := th.Server.Store().User().UpdateMfaActive(user.Id, true)
    +	require.NoError(t, err)
    +	err = th.Server.Store().User().UpdateMfaSecret(user.Id, secret.Secret)
    +	require.NoError(t, err)
    +	return secret.Secret
    +}
    +
    +func TestWebSocketMFAEnforcement(t *testing.T) {
    +	mainHelper.Parallel(t)
    +
    +	t.Run("WebSocket works when MFA enforcement is disabled", func(t *testing.T) {
    +		th := Setup(t).InitBasic()
    +		defer th.TearDown()
    +
    +		// MFA enforcement disabled - should work normally
    +		webSocketClient := th.CreateConnectedWebSocketClient(t)
    +		defer webSocketClient.Close()
    +
    +		webSocketClient.GetStatuses()
    +
    +		select {
    +		case resp := <-webSocketClient.ResponseChannel:
    +			require.Nil(t, resp.Error, "WebSocket should work when MFA enforcement is disabled")
    +			require.Equal(t, resp.Status, model.StatusOk)
    +		case <-time.After(3 * time.Second):
    +			require.Fail(t, "Expected WebSocket response but got timeout")
    +		}
    +	})
    +
    +	t.Run("WebSocket blocked when MFA required but user has no MFA", func(t *testing.T) {
    +		th := SetupEnterprise(t).InitBasic()
    +		defer th.TearDown()
    +
    +		// Enable MFA enforcement in config
    +		enableMFAEnforcement(th)
    +		// Defer the teardown to reset the config after the test
    +		defer func() {
    +			th.App.UpdateConfig(func(cfg *model.Config) {
    +				*cfg.ServiceSettings.EnforceMultifactorAuthentication = false
    +			})
    +		}()
    +
    +		// Create user without MFA using existing basic user to avoid license timing issues
    +		user := th.BasicUser
    +
    +		// Login user (this should work for initial authentication)
    +		client := th.CreateClient()
    +		_, _, err := client.Login(context.Background(), user.Email, "Pa$$word11")
    +		require.NoError(t, err)
    +
    +		// Create WebSocket client - initial connection succeeds, but subsequent API requests require completed MFA
    +		webSocketClient, err := th.CreateWebSocketClientWithClient(client)
    +		require.NoError(t, err)
    +		require.NotNil(t, webSocketClient, "webSocketClient should not be nil")
    +		webSocketClient.Listen()
    +		defer webSocketClient.Close()
    +
    +		// First, consume the successful authentication challenge response
    +		authResp := <-webSocketClient.ResponseChannel
    +		require.Nil(t, authResp.Error, "Authentication challenge should succeed")
    +		require.Equal(t, authResp.Status, model.StatusOk)
    +
    +		// Individual WebSocket requests should be blocked due to MFA requirement
    +		webSocketClient.GetStatuses()
    +
    +		// Should get authentication error due to MFA requirement on the second request
    +		select {
    +		case resp := <-webSocketClient.ResponseChannel:
    +			t.Logf("Received response: Error=%v, Status=%s, SeqReply=%d", resp.Error, resp.Status, resp.SeqReply)
    +			require.NotNil(t, resp.Error, "Should get authentication error due to MFA requirement")
    +			require.Equal(t, "api.web_socket_router.not_authenticated.app_error", resp.Error.Id,
    +				"Should get specific 'not authenticated' error ID due to MFA requirement")
    +		case <-time.After(3 * time.Second):
    +			require.Fail(t, "Expected WebSocket error response but got timeout")
    +		}
    +	})
    +
    +	t.Run("WebSocket connection allowed when user has MFA active", func(t *testing.T) {
    +		th := SetupEnterprise(t).InitBasic()
    +		defer th.TearDown()
    +
    +		// Enable MFA enforcement in config
    +		enableMFAEnforcement(th)
    +		// Defer the teardown to reset the config after the test
    +		defer func() {
    +			th.App.UpdateConfig(func(cfg *model.Config) {
    +				*cfg.ServiceSettings.EnforceMultifactorAuthentication = false
    +			})
    +		}()
    +
    +		// Create user and set up MFA
    +		user := &model.User{
    +			Email:    th.GenerateTestEmail(),
    +			Username: model.NewUsername(),
    +			Password: "password123",
    +		}
    +		ruser, _, err := th.Client.CreateUser(context.Background(), user)
    +		require.NoError(t, err)
    +
    +		th.LinkUserToTeam(ruser, th.BasicTeam)
    +		_, err = th.App.Srv().Store().User().VerifyEmail(ruser.Id, ruser.Email)
    +		require.NoError(t, err)
    +
    +		// Setup MFA for the user and get the secret
    +		secretString := setupUserWithMFA(t, th, ruser)
    +
    +		// Generate TOTP token from the user's MFA secret
    +		code := dgoogauth.ComputeCode(secretString, time.Now().UTC().Unix()/30)
    +		token := fmt.Sprintf("%06d", code)
    +
    +		client := th.CreateClient()
    +		_, _, err = client.LoginWithMFA(context.Background(), user.Email, user.Password, token)
    +		require.NoError(t, err)
    +
    +		// WebSocket connection should work
    +		webSocketClient := th.CreateConnectedWebSocketClientWithClient(t, client)
    +		defer webSocketClient.Close()
    +
    +		// Should be able to get statuses
    +		webSocketClient.GetStatuses()
    +
    +		select {
    +		case resp := <-webSocketClient.ResponseChannel:
    +			require.Nil(t, resp.Error, "WebSocket should work when MFA is properly set up")
    +			require.Equal(t, resp.Status, model.StatusOk)
    +		case <-time.After(5 * time.Second):
    +			require.Fail(t, "Expected WebSocket response but got timeout")
    +		}
    +	})
    +}
    
  • server/channels/app/platform/helper_test.go+4 0 modified
    @@ -65,6 +65,10 @@ func (ms *mockSuite) HasPermissionToReadChannel(rctx request.CTX, userID string,
     	return true
     }
     
    +func (ms *mockSuite) MFARequired(rctx request.CTX) *model.AppError {
    +	return nil
    +}
    +
     func setupDBStore(tb testing.TB) (store.Store, *model.SqlSettings) {
     	var dbStore store.Store
     	var dbSettings *model.SqlSettings
    
  • server/channels/app/platform/mocks/SuiteIFace.go+20 0 modified
    @@ -66,6 +66,26 @@ func (_m *SuiteIFace) HasPermissionToReadChannel(rctx request.CTX, userID string
     	return r0
     }
     
    +// MFARequired provides a mock function with given fields: c
    +func (_m *SuiteIFace) MFARequired(rctx request.CTX) *model.AppError {
    +	ret := _m.Called(rctx)
    +
    +	if len(ret) == 0 {
    +		panic("no return value specified for MFARequired")
    +	}
    +
    +	var r0 *model.AppError
    +	if rf, ok := ret.Get(0).(func(request.CTX) *model.AppError); ok {
    +		r0 = rf(rctx)
    +	} else {
    +		if ret.Get(0) != nil {
    +			r0 = ret.Get(0).(*model.AppError)
    +		}
    +	}
    +
    +	return r0
    +}
    +
     // RolesGrantPermission provides a mock function with given fields: roleNames, permissionId
     func (_m *SuiteIFace) RolesGrantPermission(roleNames []string, permissionId string) bool {
     	ret := _m.Called(roleNames, permissionId)
    
  • server/channels/app/platform/web_conn.go+22 4 modified
    @@ -453,7 +453,7 @@ func (wc *WebConn) readPump() {
     		if err := wc.WebSocket.SetReadDeadline(time.Now().Add(pongWaitTime)); err != nil {
     			return err
     		}
    -		if wc.IsAuthenticated() {
    +		if wc.IsBasicAuthenticated() {
     			userID := wc.UserId
     			wc.Platform.Go(func() {
     				wc.Platform.SetStatusAwayIfNeeded(userID, false)
    @@ -770,8 +770,8 @@ func (wc *WebConn) InvalidateCache() {
     	wc.SetSessionExpiresAt(0)
     }
     
    -// IsAuthenticated returns whether the given WebConn is authenticated or not.
    -func (wc *WebConn) IsAuthenticated() bool {
    +// IsBasicAuthenticated returns whether the given WebConn has a valid session.
    +func (wc *WebConn) IsBasicAuthenticated() bool {
     	// Check the expiry to see if we need to check for a new session
     	if wc.GetSessionExpiresAt() < model.GetMillis() {
     		if wc.GetSessionToken() == "" {
    @@ -799,6 +799,24 @@ func (wc *WebConn) IsAuthenticated() bool {
     	return true
     }
     
    +// IsMFAAuthenticated returns whether the user has completed MFA when required.
    +func (wc *WebConn) IsMFAAuthenticated() bool {
    +	session := wc.GetSession()
    +	c := request.EmptyContext(wc.Platform.logger).WithSession(session)
    +
    +	// Check if MFA is required and user has NOT completed MFA
    +	if appErr := wc.Suite.MFARequired(c); appErr != nil {
    +		return false
    +	}
    +
    +	return true
    +}
    +
    +// IsAuthenticated returns whether the given WebConn is fully authenticated (session + MFA).
    +func (wc *WebConn) IsAuthenticated() bool {
    +	return wc.IsBasicAuthenticated() && wc.IsMFAAuthenticated()
    +}
    +
     func (wc *WebConn) createHelloMessage() *model.WebSocketEvent {
     	ee := wc.Platform.LicenseManager() != nil
     
    @@ -856,7 +874,7 @@ func (wc *WebConn) ShouldSendEventToGuest(msg *model.WebSocketEvent) bool {
     
     // ShouldSendEvent returns whether the message should be sent or not.
     func (wc *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
    -	// IMPORTANT: Do not send event if WebConn does not have a session
    +	// IMPORTANT: Do not send event if WebConn does not have a session and completed MFA
     	if !wc.IsAuthenticated() {
     		return false
     	}
    
  • server/channels/app/platform/web_hub.go+2 1 modified
    @@ -30,6 +30,7 @@ type SuiteIFace interface {
     	RolesGrantPermission(roleNames []string, permissionId string) bool
     	HasPermissionToReadChannel(rctx request.CTX, userID string, channel *model.Channel) bool
     	UserCanSeeOtherUser(rctx request.CTX, userID string, otherUserId string) (bool, *model.AppError)
    +	MFARequired(rctx request.CTX) *model.AppError
     }
     
     type webConnActivityMessage struct {
    @@ -572,7 +573,7 @@ func (h *Hub) Start() {
     				}
     				atomic.StoreInt64(&h.connectionCount, int64(connIndex.AllActive()))
     
    -				if webConnReg.conn.IsAuthenticated() && webConnReg.conn.reuseCount == 0 {
    +				if webConnReg.conn.IsBasicAuthenticated() && webConnReg.conn.reuseCount == 0 {
     					// The hello message should only be sent when the reuseCount is 0.
     					// i.e in server restart, or long timeout, or fresh connection case.
     					// In case of seq number not found in dead queue, it is handled by
    
  • server/channels/app/platform/web_hub_test.go+3 0 modified
    @@ -18,6 +18,7 @@ import (
     
     	"github.com/gorilla/websocket"
     	"github.com/stretchr/testify/assert"
    +	"github.com/stretchr/testify/mock"
     	"github.com/stretchr/testify/require"
     
     	"github.com/mattermost/mattermost/server/public/model"
    @@ -598,6 +599,7 @@ func TestHubIsRegistered(t *testing.T) {
     
     	mockSuite := &platform_mocks.SuiteIFace{}
     	mockSuite.On("GetSession", session.Token).Return(session, nil)
    +	mockSuite.On("MFARequired", mock.Anything).Return(nil)
     	th.Suite = mockSuite
     
     	s := httptest.NewServer(dummyWebsocketHandler(t))
    @@ -633,6 +635,7 @@ func TestHubWebConnCount(t *testing.T) {
     
     	mockSuite := &platform_mocks.SuiteIFace{}
     	mockSuite.On("GetSession", session.Token).Return(session, nil)
    +	mockSuite.On("MFARequired", mock.Anything).Return(nil)
     	th.Suite = mockSuite
     
     	s := httptest.NewServer(dummyWebsocketHandler(t))
    

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.