Reject boards in id-batch lookups, unread, and member-mutation endpoints

- GetChannelsByIds, GetChannelsWithTeamDataByIds, and GetChannelUnread
  now exclude BO/BP at the store layer so boards can't slip through if
  callers stop filtering first.
- updateChannelMemberNotifyProps, updateChannelMemberAutotranslation,
  and viewChannel now reject board IDs explicitly via the existing
  rejectBoardChannelByID helper, matching the other write endpoints.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Julien Tant
2026-04-30 12:16:33 -07:00
parent bff24f0290
commit a899530155
3 changed files with 38 additions and 2 deletions
+15
View File
@@ -1808,6 +1808,13 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if view.ChannelId != "" && rejectBoardChannelByID(c, view.ChannelId) {
return
}
if view.PrevChannelId != "" && rejectBoardChannelByID(c, view.PrevChannelId) {
return
}
times, err := c.App.ViewChannel(c.AppContext, &view, c.Params.UserId, c.AppContext.Session().Id, view.CollapsedThreadsSupported)
if err != nil {
c.Err = err
@@ -1941,6 +1948,10 @@ func updateChannelMemberNotifyProps(c *Context, w http.ResponseWriter, r *http.R
return
}
if rejectBoardChannelByID(c, c.Params.ChannelId) {
return
}
props := model.MapFromJSON(r.Body)
if props == nil {
c.SetInvalidParam("notify_props")
@@ -1983,6 +1994,10 @@ func updateChannelMemberAutotranslation(c *Context, w http.ResponseWriter, r *ht
return
}
if rejectBoardChannelByID(c, c.Params.ChannelId) {
return
}
props := UpdateChannelMemberAutotranslationProps{}
if err := json.NewDecoder(r.Body).Decode(&props); err != nil {
c.SetInvalidParamWithErr("autotranslation_disabled", err)
+18
View File
@@ -7102,6 +7102,24 @@ func TestChannelEndpointsRejectBoards(t *testing.T) {
CheckForbiddenStatus(t, resp) // license check fires before channel fetch
})
t.Run("updateChannelMemberNotifyProps rejects board", func(t *testing.T) {
resp, err := client.UpdateChannelNotifyProps(ctx, boardChannel.Id, th.BasicUser.Id, map[string]string{model.DesktopNotifyProp: model.UserNotifyAll})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("updateChannelMemberAutotranslation rejects board", func(t *testing.T) {
resp, err := client.UpdateChannelMemberAutotranslation(ctx, boardChannel.Id, th.BasicUser.Id, true)
require.Error(t, err)
CheckForbiddenStatus(t, resp) // feature-availability check fires before channel fetch
})
t.Run("viewChannel rejects board", func(t *testing.T) {
_, resp, err := client.ViewChannel(ctx, th.BasicUser.Id, &model.ChannelView{ChannelId: boardChannel.Id})
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
// --- READ operations: boards are invisible (404) via store-level filter ---
t.Run("getChannel not found for board", func(t *testing.T) {
@@ -900,8 +900,9 @@ func (s SqlChannelStore) GetChannelUnread(channelId, userId string) (*model.Chan
Id = ChannelId
AND Id = ?
AND UserId = ?
AND DeleteAt = 0`,
channelId, userId)
AND DeleteAt = 0
AND Channels.Type NOT IN (?, ?)`,
channelId, userId, model.ChannelTypeOpenBoard, model.ChannelTypePrivateBoard)
if err != nil {
if err == sql.ErrNoRows {
return nil, store.NewErrNotFound("Channel", fmt.Sprintf("channelId=%s,userId=%s", channelId, userId))
@@ -2893,6 +2894,7 @@ func (s SqlChannelStore) GetChannelsByIds(channelIds []string, includeDeleted bo
Select(channelSliceColumns(true)...).
From("Channels").
Where(sq.Eq{"Id": channelIds}).
Where(sq.NotEq{"Type": []model.ChannelType{model.ChannelTypeOpenBoard, model.ChannelTypePrivateBoard}}).
OrderBy("Name")
if !includeDeleted {
@@ -2923,6 +2925,7 @@ func (s SqlChannelStore) GetChannelsWithTeamDataByIds(channelIDs []string, inclu
From("Channels c").
LeftJoin("Teams t ON c.TeamId = t.Id").
Where(sq.Eq{"c.Id": channelIDs}).
Where(sq.NotEq{"c.Type": []model.ChannelType{model.ChannelTypeOpenBoard, model.ChannelTypePrivateBoard}}).
OrderBy("c.Name")
if !includeDeleted {