CVE-2024-4182
Description
Mattermost versions 9.6.0, 9.5.x before 9.5.3, 9.4.x before 9.4.5, and 8.1.x before 8.1.12 fail to handle JSON parsing errors in custom status values, which allows an authenticated attacker to crash other users' web clients via a malformed custom status.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/mattermost/mattermost-serverGo | >= 8.1.0, < 8.1.12 | 8.1.12 |
github.com/mattermost/mattermost-serverGo | >= 9.4.0, < 9.4.5 | 9.4.5 |
github.com/mattermost/mattermost-serverGo | >= 9.5.0, < 9.5.3 | 9.5.3 |
github.com/mattermost/mattermost-serverGo | >= 9.6.0-rc1, < 9.6.1 | 9.6.1 |
Affected products
1- Range: 9.6.0
Patches
46cbab0f7ece1MM-56881 Validate and ensure valid CustomStatus is stored (#26287) (#26494)
7 files changed · +78 −8
server/i18n/en.json+4 −0 modified@@ -9259,6 +9259,10 @@ "id": "model.user.is_valid.id.app_error", "translation": "Invalid user id." }, + { + "id": "model.user.is_valid.invalidProperty.app_error", + "translation": "Invalid props (custom status)" + }, { "id": "model.user.is_valid.last_name.app_error", "translation": "Invalid last name."
server/public/model/custom_status.go+0 −4 modified@@ -37,10 +37,6 @@ type CustomStatus struct { } func (cs *CustomStatus) PreSave() { - if cs.Emoji == "" { - cs.Emoji = DefaultCustomStatusEmoji - } - if cs.Duration == "" && !cs.ExpiresAt.Before(time.Now()) { cs.Duration = "date_and_time" }
server/public/model/user.go+32 −0 modified@@ -392,6 +392,13 @@ func (u *User) IsValid() *AppError { map[string]any{"Limit": UserRolesMaxLength}, "user_id="+u.Id+" roles_limit="+u.Roles, http.StatusBadRequest) } + if u.Props != nil { + if !u.ValidateCustomStatus() { + return NewAppError("User.IsValid", "model.user.is_valid.invalidProperty.app_error", + map[string]any{"Props": u.Props}, "user_id="+u.Id, http.StatusBadRequest) + } + } + return nil } @@ -465,6 +472,12 @@ func (u *User) PreSave() { if u.Password != "" { u.Password = HashPassword(u.Password) } + + cs := u.GetCustomStatus() + if cs != nil { + cs.PreSave() + u.SetCustomStatus(cs) + } } // The following are some GraphQL methods necessary to return the @@ -542,6 +555,14 @@ func (u *User) PreUpdate() { } u.NotifyProps[MentionKeysNotifyProp] = strings.Join(goodKeys, ",") } + + if u.Props != nil { + cs := u.GetCustomStatus() + if cs != nil { + cs.PreSave() + u.SetCustomStatus(cs) + } + } } func (u *User) SetDefaultNotifications() { @@ -744,6 +765,17 @@ func (u *User) ClearCustomStatus() { u.Props[UserPropsKeyCustomStatus] = "" } +func (u *User) ValidateCustomStatus() bool { + status, exists := u.Props[UserPropsKeyCustomStatus] + if exists && status != "" { + cs := u.GetCustomStatus() + if cs == nil { + return false + } + } + return true +} + func (u *User) GetFullName() string { if u.FirstName != "" && u.LastName != "" { return u.FirstName + " " + u.LastName
server/public/model/user_test.go+21 −0 modified@@ -353,3 +353,24 @@ func TestUserSlice(t *testing.T) { assert.Len(t, nonBotUsers, 1) }) } + +func TestValidateCustomStatus(t *testing.T) { + t.Run("ValidateCustomStatus", func(t *testing.T) { + user0 := &User{Id: "user0", DeleteAt: 0, IsBot: true} + + user0.Props = map[string]string{UserPropsKeyCustomStatus: ""} + assert.True(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "hello" + assert.False(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "{\"emoji\":{\"foo\":\"bar\"}}" + assert.True(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "{\"text\": \"hello\"}" + assert.True(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "{\"wrong\": \"hello\"}" + assert.True(t, user0.ValidateCustomStatus()) + }) +}
webapp/channels/src/components/status_dropdown/status_dropdown.tsx+3 −3 modified@@ -300,7 +300,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> { time={customStatus.expires_at} timezone={this.props.timezone} className={classNames('custom_status__expiry', { - padded: customStatus?.text.length > 0, + padded: customStatus?.text?.length > 0, })} withinBrackets={true} /> @@ -313,7 +313,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> { modalId={ModalIdentifiers.CUSTOM_STATUS} dialogType={CustomStatusModal} className={classNames('MenuItem__primary-text custom_status__row', { - flex: customStatus?.text.length === 0, + flex: customStatus?.text?.length === 0, })} id={'status-menu-custom-status'} > @@ -344,7 +344,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> { const {intl} = this.props; const needsConfirm = this.isUserOutOfOffice() && this.props.autoResetPref === ''; const {status, customStatus, isCustomStatusExpired, currentUser} = this.props; - const isStatusSet = customStatus && (customStatus.text.length > 0 || customStatus.emoji.length > 0) && !isCustomStatusExpired; + const isStatusSet = customStatus && !isCustomStatusExpired && (customStatus.text?.length > 0 || customStatus.emoji?.length > 0); const setOnline = needsConfirm ? () => this.showStatusChangeConfirmation('online') : this.setOnline; const setDnd = needsConfirm ? () => this.showStatusChangeConfirmation('dnd') : this.setDnd;
webapp/channels/src/selectors/views/custom_status.test.ts+9 −0 modified@@ -39,6 +39,15 @@ describe('getCustomStatus', () => { expect(getCustomStatus(store.getState(), user.id)).toBeUndefined(); }); + it('should return undefined when user with invalid json for custom status set', async () => { + const store = await configureStore(); + const newUser = {...user}; + newUser.props.customStatus = 'not a JSON string'; + + (UserSelectors.getUser as jest.Mock).mockReturnValue(user); + expect(getCustomStatus(store.getState(), user.id)).toBeUndefined(); + }); + it('should return customStatus object when there is custom status set', async () => { const store = await configureStore(); const newUser = {...user};
webapp/channels/src/selectors/views/custom_status.ts+9 −1 modified@@ -24,7 +24,15 @@ export function makeGetCustomStatus(): (state: GlobalState, userID?: string) => (state: GlobalState, userID?: string) => (userID ? getUser(state, userID) : getCurrentUser(state)), (user) => { const userProps = user?.props || {}; - return userProps.customStatus ? JSON.parse(userProps.customStatus) : undefined; + let customStatus; + if (userProps.customStatus) { + try { + customStatus = JSON.parse(userProps.customStatus); + } catch (error) { + // do nothing if invalid, return undefined custom status. + } + } + return customStatus; }, ); }
f84f8ed65f6aMM-56881 Validate and ensure valid CustomStatus is stored (#26287) (#26446)
7 files changed · +78 −8
server/i18n/en.json+4 −0 modified@@ -9446,6 +9446,10 @@ "id": "model.user.is_valid.id.app_error", "translation": "Invalid user id." }, + { + "id": "model.user.is_valid.invalidProperty.app_error", + "translation": "Invalid props (custom status)" + }, { "id": "model.user.is_valid.last_name.app_error", "translation": "Invalid last name."
server/public/model/custom_status.go+0 −4 modified@@ -35,10 +35,6 @@ type CustomStatus struct { } func (cs *CustomStatus) PreSave() { - if cs.Emoji == "" { - cs.Emoji = DefaultCustomStatusEmoji - } - if cs.Duration == "" && !cs.ExpiresAt.Before(time.Now()) { cs.Duration = "date_and_time" }
server/public/model/user.go+32 −0 modified@@ -399,6 +399,13 @@ func (u *User) IsValid() *AppError { map[string]any{"Limit": UserRolesMaxLength}, "user_id="+u.Id+" roles_limit="+u.Roles, http.StatusBadRequest) } + if u.Props != nil { + if !u.ValidateCustomStatus() { + return NewAppError("User.IsValid", "model.user.is_valid.invalidProperty.app_error", + map[string]any{"Props": u.Props}, "user_id="+u.Id, http.StatusBadRequest) + } + } + return nil } @@ -472,6 +479,12 @@ func (u *User) PreSave() { if u.Password != "" { u.Password = HashPassword(u.Password) } + + cs := u.GetCustomStatus() + if cs != nil { + cs.PreSave() + u.SetCustomStatus(cs) + } } // PreUpdate should be run before updating the user in the db. @@ -508,6 +521,14 @@ func (u *User) PreUpdate() { } u.NotifyProps[MentionKeysNotifyProp] = strings.Join(goodKeys, ",") } + + if u.Props != nil { + cs := u.GetCustomStatus() + if cs != nil { + cs.PreSave() + u.SetCustomStatus(cs) + } + } } func (u *User) SetDefaultNotifications() { @@ -711,6 +732,17 @@ func (u *User) ClearCustomStatus() { u.Props[UserPropsKeyCustomStatus] = "" } +func (u *User) ValidateCustomStatus() bool { + status, exists := u.Props[UserPropsKeyCustomStatus] + if exists && status != "" { + cs := u.GetCustomStatus() + if cs == nil { + return false + } + } + return true +} + func (u *User) GetFullName() string { if u.FirstName != "" && u.LastName != "" { return u.FirstName + " " + u.LastName
server/public/model/user_test.go+21 −0 modified@@ -355,3 +355,24 @@ func TestUserSlice(t *testing.T) { assert.Len(t, nonBotUsers, 1) }) } + +func TestValidateCustomStatus(t *testing.T) { + t.Run("ValidateCustomStatus", func(t *testing.T) { + user0 := &User{Id: "user0", DeleteAt: 0, IsBot: true} + + user0.Props = map[string]string{UserPropsKeyCustomStatus: ""} + assert.True(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "hello" + assert.False(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "{\"emoji\":{\"foo\":\"bar\"}}" + assert.True(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "{\"text\": \"hello\"}" + assert.True(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "{\"wrong\": \"hello\"}" + assert.True(t, user0.ValidateCustomStatus()) + }) +}
webapp/channels/src/components/status_dropdown/status_dropdown.tsx+3 −3 modified@@ -304,7 +304,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> { time={customStatus.expires_at} timezone={this.props.timezone} className={classNames('custom_status__expiry', { - padded: customStatus?.text.length > 0, + padded: customStatus?.text?.length > 0, })} withinBrackets={true} /> @@ -317,7 +317,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> { modalId={ModalIdentifiers.CUSTOM_STATUS} dialogType={CustomStatusModal} className={classNames('MenuItem__primary-text custom_status__row', { - flex: customStatus?.text.length === 0, + flex: customStatus?.text?.length === 0, })} id={'status-menu-custom-status'} > @@ -348,7 +348,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> { const {intl} = this.props; const needsConfirm = this.isUserOutOfOffice() && this.props.autoResetPref === ''; const {status, customStatus, isCustomStatusExpired, currentUser} = this.props; - const isStatusSet = customStatus && (customStatus.text.length > 0 || customStatus.emoji.length > 0) && !isCustomStatusExpired; + const isStatusSet = customStatus && !isCustomStatusExpired && (customStatus.text?.length > 0 || customStatus.emoji?.length > 0); const setOnline = needsConfirm ? () => this.showStatusChangeConfirmation('online') : this.setOnline; const setDnd = needsConfirm ? () => this.showStatusChangeConfirmation('dnd') : this.setDnd;
webapp/channels/src/selectors/views/custom_status.test.ts+9 −0 modified@@ -40,6 +40,15 @@ describe('getCustomStatus', () => { expect(getCustomStatus(store.getState(), user.id)).toBeUndefined(); }); + it('should return undefined when user with invalid json for custom status set', async () => { + const store = await configureStore(); + const newUser = {...user}; + newUser.props.customStatus = 'not a JSON string'; + + (UserSelectors.getUser as jest.Mock).mockReturnValue(user); + expect(getCustomStatus(store.getState(), user.id)).toBeUndefined(); + }); + it('should return customStatus object when there is custom status set', async () => { const store = await configureStore(); const newUser = {...user};
webapp/channels/src/selectors/views/custom_status.ts+9 −1 modified@@ -26,7 +26,15 @@ export function makeGetCustomStatus(): (state: GlobalState, userID?: string) => (state: GlobalState, userID?: string) => (userID ? getUser(state, userID) : getCurrentUser(state)), (user) => { const userProps = user?.props || {}; - return userProps.customStatus ? JSON.parse(userProps.customStatus) : undefined; + let customStatus; + if (userProps.customStatus) { + try { + customStatus = JSON.parse(userProps.customStatus); + } catch (error) { + // do nothing if invalid, return undefined custom status. + } + } + return customStatus; }, ); }
a99dadd80c57MM-56881 Validate and ensure valid CustomStatus is stored (#26287) (#26445)
7 files changed · +78 −8
server/i18n/en.json+4 −0 modified@@ -9626,6 +9626,10 @@ "id": "model.user.is_valid.id.app_error", "translation": "Invalid user id." }, + { + "id": "model.user.is_valid.invalidProperty.app_error", + "translation": "Invalid props (custom status)" + }, { "id": "model.user.is_valid.last_name.app_error", "translation": "Invalid last name."
server/public/model/custom_status.go+0 −4 modified@@ -35,10 +35,6 @@ type CustomStatus struct { } func (cs *CustomStatus) PreSave() { - if cs.Emoji == "" { - cs.Emoji = DefaultCustomStatusEmoji - } - if cs.Duration == "" && !cs.ExpiresAt.Before(time.Now()) { cs.Duration = "date_and_time" }
server/public/model/user.go+32 −0 modified@@ -399,6 +399,13 @@ func (u *User) IsValid() *AppError { map[string]any{"Limit": UserRolesMaxLength}, "user_id="+u.Id+" roles_limit="+u.Roles, http.StatusBadRequest) } + if u.Props != nil { + if !u.ValidateCustomStatus() { + return NewAppError("User.IsValid", "model.user.is_valid.invalidProperty.app_error", + map[string]any{"Props": u.Props}, "user_id="+u.Id, http.StatusBadRequest) + } + } + return nil } @@ -472,6 +479,12 @@ func (u *User) PreSave() { if u.Password != "" { u.Password = HashPassword(u.Password) } + + cs := u.GetCustomStatus() + if cs != nil { + cs.PreSave() + u.SetCustomStatus(cs) + } } // PreUpdate should be run before updating the user in the db. @@ -508,6 +521,14 @@ func (u *User) PreUpdate() { } u.NotifyProps[MentionKeysNotifyProp] = strings.Join(goodKeys, ",") } + + if u.Props != nil { + cs := u.GetCustomStatus() + if cs != nil { + cs.PreSave() + u.SetCustomStatus(cs) + } + } } func (u *User) SetDefaultNotifications() { @@ -711,6 +732,17 @@ func (u *User) ClearCustomStatus() { u.Props[UserPropsKeyCustomStatus] = "" } +func (u *User) ValidateCustomStatus() bool { + status, exists := u.Props[UserPropsKeyCustomStatus] + if exists && status != "" { + cs := u.GetCustomStatus() + if cs == nil { + return false + } + } + return true +} + func (u *User) GetFullName() string { if u.FirstName != "" && u.LastName != "" { return u.FirstName + " " + u.LastName
server/public/model/user_test.go+21 −0 modified@@ -355,3 +355,24 @@ func TestUserSlice(t *testing.T) { assert.Len(t, nonBotUsers, 1) }) } + +func TestValidateCustomStatus(t *testing.T) { + t.Run("ValidateCustomStatus", func(t *testing.T) { + user0 := &User{Id: "user0", DeleteAt: 0, IsBot: true} + + user0.Props = map[string]string{UserPropsKeyCustomStatus: ""} + assert.True(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "hello" + assert.False(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "{\"emoji\":{\"foo\":\"bar\"}}" + assert.True(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "{\"text\": \"hello\"}" + assert.True(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "{\"wrong\": \"hello\"}" + assert.True(t, user0.ValidateCustomStatus()) + }) +}
webapp/channels/src/components/status_dropdown/status_dropdown.tsx+3 −3 modified@@ -302,7 +302,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> { time={customStatus.expires_at} timezone={this.props.timezone} className={classNames('custom_status__expiry', { - padded: customStatus?.text.length > 0, + padded: customStatus?.text?.length > 0, })} withinBrackets={true} /> @@ -315,7 +315,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> { modalId={ModalIdentifiers.CUSTOM_STATUS} dialogType={CustomStatusModal} className={classNames('MenuItem__primary-text custom_status__row', { - flex: customStatus?.text.length === 0, + flex: customStatus?.text?.length === 0, })} id={'status-menu-custom-status'} > @@ -346,7 +346,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> { const {intl} = this.props; const needsConfirm = this.isUserOutOfOffice() && this.props.autoResetPref === ''; const {status, customStatus, isCustomStatusExpired, currentUser} = this.props; - const isStatusSet = customStatus && (customStatus.text.length > 0 || customStatus.emoji.length > 0) && !isCustomStatusExpired; + const isStatusSet = customStatus && !isCustomStatusExpired && (customStatus.text?.length > 0 || customStatus.emoji?.length > 0); const setOnline = needsConfirm ? () => this.showStatusChangeConfirmation('online') : this.setOnline; const setDnd = needsConfirm ? () => this.showStatusChangeConfirmation('dnd') : this.setDnd;
webapp/channels/src/selectors/views/custom_status.test.ts+9 −0 modified@@ -40,6 +40,15 @@ describe('getCustomStatus', () => { expect(getCustomStatus(store.getState(), user.id)).toBeUndefined(); }); + it('should return undefined when user with invalid json for custom status set', async () => { + const store = await configureStore(); + const newUser = {...user}; + newUser.props.customStatus = 'not a JSON string'; + + (UserSelectors.getUser as jest.Mock).mockReturnValue(user); + expect(getCustomStatus(store.getState(), user.id)).toBeUndefined(); + }); + it('should return customStatus object when there is custom status set', async () => { const store = await configureStore(); const newUser = {...user};
webapp/channels/src/selectors/views/custom_status.ts+9 −1 modified@@ -26,7 +26,15 @@ export function makeGetCustomStatus(): (state: GlobalState, userID?: string) => (state: GlobalState, userID?: string) => (userID ? getUser(state, userID) : getCurrentUser(state)), (user) => { const userProps = user?.props || {}; - return userProps.customStatus ? JSON.parse(userProps.customStatus) : undefined; + let customStatus; + if (userProps.customStatus) { + try { + customStatus = JSON.parse(userProps.customStatus); + } catch (error) { + // do nothing if invalid, return undefined custom status. + } + } + return customStatus; }, ); }
41333a0babf5MM-56881 Validate and ensure valid CustomStatus is stored (#26287) (#26444)
7 files changed · +78 −8
server/i18n/en.json+4 −0 modified@@ -9774,6 +9774,10 @@ "id": "model.user.is_valid.id.app_error", "translation": "Invalid user id." }, + { + "id": "model.user.is_valid.invalidProperty.app_error", + "translation": "Invalid props (custom status)" + }, { "id": "model.user.is_valid.last_name.app_error", "translation": "Invalid last name."
server/public/model/custom_status.go+0 −4 modified@@ -35,10 +35,6 @@ type CustomStatus struct { } func (cs *CustomStatus) PreSave() { - if cs.Emoji == "" { - cs.Emoji = DefaultCustomStatusEmoji - } - if cs.Duration == "" && !cs.ExpiresAt.Before(time.Now()) { cs.Duration = "date_and_time" }
server/public/model/user.go+32 −0 modified@@ -399,6 +399,13 @@ func (u *User) IsValid() *AppError { map[string]any{"Limit": UserRolesMaxLength}, "user_id="+u.Id+" roles_limit="+u.Roles, http.StatusBadRequest) } + if u.Props != nil { + if !u.ValidateCustomStatus() { + return NewAppError("User.IsValid", "model.user.is_valid.invalidProperty.app_error", + map[string]any{"Props": u.Props}, "user_id="+u.Id, http.StatusBadRequest) + } + } + return nil } @@ -472,6 +479,12 @@ func (u *User) PreSave() { if u.Password != "" { u.Password = HashPassword(u.Password) } + + cs := u.GetCustomStatus() + if cs != nil { + cs.PreSave() + u.SetCustomStatus(cs) + } } // PreUpdate should be run before updating the user in the db. @@ -508,6 +521,14 @@ func (u *User) PreUpdate() { } u.NotifyProps[MentionKeysNotifyProp] = strings.Join(goodKeys, ",") } + + if u.Props != nil { + cs := u.GetCustomStatus() + if cs != nil { + cs.PreSave() + u.SetCustomStatus(cs) + } + } } func (u *User) SetDefaultNotifications() { @@ -711,6 +732,17 @@ func (u *User) ClearCustomStatus() { u.Props[UserPropsKeyCustomStatus] = "" } +func (u *User) ValidateCustomStatus() bool { + status, exists := u.Props[UserPropsKeyCustomStatus] + if exists && status != "" { + cs := u.GetCustomStatus() + if cs == nil { + return false + } + } + return true +} + func (u *User) GetFullName() string { if u.FirstName != "" && u.LastName != "" { return u.FirstName + " " + u.LastName
server/public/model/user_test.go+21 −0 modified@@ -355,3 +355,24 @@ func TestUserSlice(t *testing.T) { assert.Len(t, nonBotUsers, 1) }) } + +func TestValidateCustomStatus(t *testing.T) { + t.Run("ValidateCustomStatus", func(t *testing.T) { + user0 := &User{Id: "user0", DeleteAt: 0, IsBot: true} + + user0.Props = map[string]string{UserPropsKeyCustomStatus: ""} + assert.True(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "hello" + assert.False(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "{\"emoji\":{\"foo\":\"bar\"}}" + assert.True(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "{\"text\": \"hello\"}" + assert.True(t, user0.ValidateCustomStatus()) + + user0.Props[UserPropsKeyCustomStatus] = "{\"wrong\": \"hello\"}" + assert.True(t, user0.ValidateCustomStatus()) + }) +}
webapp/channels/src/components/status_dropdown/status_dropdown.tsx+3 −3 modified@@ -342,7 +342,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> { time={customStatus.expires_at} timezone={this.props.timezone} className={classNames('custom_status__expiry', { - padded: customStatus?.text.length > 0, + padded: customStatus?.text?.length > 0, })} withinBrackets={true} /> @@ -355,7 +355,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> { modalId={ModalIdentifiers.CUSTOM_STATUS} dialogType={CustomStatusModal} className={classNames('MenuItem__primary-text custom_status__row', { - flex: customStatus?.text.length === 0, + flex: customStatus?.text?.length === 0, })} id={'status-menu-custom-status'} > @@ -385,7 +385,7 @@ export class StatusDropdown extends React.PureComponent<Props, State> { const {intl} = this.props; const needsConfirm = this.isUserOutOfOffice() && this.props.autoResetPref === ''; const {status, customStatus, isCustomStatusExpired, currentUser} = this.props; - const isStatusSet = customStatus && (customStatus.text.length > 0 || customStatus.emoji.length > 0) && !isCustomStatusExpired; + const isStatusSet = customStatus && !isCustomStatusExpired && (customStatus.text?.length > 0 || customStatus.emoji?.length > 0); const setOnline = needsConfirm ? () => this.showStatusChangeConfirmation('online') : this.setOnline; const setDnd = needsConfirm ? () => this.showStatusChangeConfirmation('dnd') : this.setDnd;
webapp/channels/src/selectors/views/custom_status.test.ts+9 −0 modified@@ -40,6 +40,15 @@ describe('getCustomStatus', () => { expect(getCustomStatus(store.getState(), user.id)).toBeUndefined(); }); + it('should return undefined when user with invalid json for custom status set', async () => { + const store = await configureStore(); + const newUser = {...user}; + newUser.props.customStatus = 'not a JSON string'; + + (UserSelectors.getUser as jest.Mock).mockReturnValue(user); + expect(getCustomStatus(store.getState(), user.id)).toBeUndefined(); + }); + it('should return customStatus object when there is custom status set', async () => { const store = await configureStore(); const newUser = {...user};
webapp/channels/src/selectors/views/custom_status.ts+9 −1 modified@@ -26,7 +26,15 @@ export function makeGetCustomStatus(): (state: GlobalState, userID?: string) => (state: GlobalState, userID?: string) => (userID ? getUser(state, userID) : getCurrentUser(state)), (user) => { const userProps = user?.props || {}; - return userProps.customStatus ? JSON.parse(userProps.customStatus) : undefined; + let customStatus; + if (userProps.customStatus) { + try { + customStatus = JSON.parse(userProps.customStatus); + } catch (error) { + // do nothing if invalid, return undefined custom status. + } + } + return customStatus; }, ); }
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
7- github.com/advisories/GHSA-8f99-g2pj-x8w3ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-4182ghsaADVISORY
- github.com/mattermost/mattermost/commit/41333a0babf565453d89287549bec1e546e75ce7ghsaWEB
- github.com/mattermost/mattermost/commit/6cbab0f7ece104681f73dd12c75d9f22d567125eghsaWEB
- github.com/mattermost/mattermost/commit/a99dadd80c57d376185ca06f8f70919a6f135bc6ghsaWEB
- github.com/mattermost/mattermost/commit/f84f8ed65f6a5faba974426424b684635455a527ghsaWEB
- mattermost.com/security-updatesghsaWEB
News mentions
0No linked articles in our index yet.