[release-10.11] MM-69010: Validate incoming webhook user membership (#36917)

Automatic Merge
Этот коммит содержится в:
Maria A Nunez
2026-06-05 03:59:53 -04:00
коммит произвёл GitHub
родитель beaa59db54
Коммит f6d3a7827e
5 изменённых файлов: 160 добавлений и 1 удалений

Просмотреть файл

@@ -387,6 +387,29 @@ func (a *App) CreateWebhookPost(c request.CTX, userID string, channel *model.Cha
return splits[0], nil
}
// ValidateIncomingWebhookUser ensures a user being assigned as an incoming webhook's owner can
// legitimately be attributed posts in the target channel: the user must have access to the
// channel and must not hold privileges the requester lacks, so a requester cannot forge posts
// as a non-member or higher-privileged user.
func (a *App) ValidateIncomingWebhookUser(rctx request.CTX, session model.Session, user *model.User, channel *model.Channel) *model.AppError {
if user.IsSystemAdmin() && !a.SessionHasPermissionTo(session, model.PermissionManageSystem) {
return model.NewAppError("ValidateIncomingWebhookUser", "api.webhook.incoming.user_role.app_error", nil, "user_id="+user.Id, http.StatusForbidden)
}
return a.ValidateIncomingWebhookUserChannelAccess(rctx, user.Id, channel)
}
// ValidateIncomingWebhookUserChannelAccess ensures the webhook owner can read the channel its
// posts are attributed to, preventing attribution to a user who is not a member of the channel
// (or its team, for open channels).
func (a *App) ValidateIncomingWebhookUserChannelAccess(rctx request.CTX, userID string, channel *model.Channel) *model.AppError {
if hasPermission, _ := a.HasPermissionToChannel(rctx, userID, channel.Id, model.PermissionReadChannelContent); !hasPermission {
return model.NewAppError("ValidateIncomingWebhookUserChannelAccess", "api.webhook.incoming.user_membership.app_error", nil, "user_id="+userID+", channel_id="+channel.Id, http.StatusForbidden)
}
return nil
}
func (a *App) CreateIncomingWebhookForChannel(creatorId string, channel *model.Channel, hook *model.IncomingWebhook) (*model.IncomingWebhook, *model.AppError) {
if !*a.Config().ServiceSettings.EnableIncomingWebhooks {
return nil, model.NewAppError("CreateIncomingWebhookForChannel", "api.incoming_webhook.disabled.app_error", nil, "", http.StatusNotImplemented)
@@ -797,6 +820,17 @@ func (a *App) HandleIncomingWebhook(c request.CTX, hookID string, req *model.Inc
if nErr != nil {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.user.app_error", map[string]any{"user": channelName[1:]}, "", http.StatusBadRequest).Wrap(nErr)
}
// Only allow a DM target the webhook owner shares a team with, so the stored
// user_id cannot be used to reach users the owner could not message directly.
if hook.UserId != result.Id {
commonTeamIDs, teamErr := a.GetCommonTeamIDsForTwoUsers(hook.UserId, result.Id)
if teamErr != nil {
return teamErr
}
if len(commonTeamIDs) == 0 {
return model.NewAppError("HandleIncomingWebhook", "web.incoming_webhook.permissions.app_error", map[string]any{"user": hook.UserId, "channel": channelName}, "", http.StatusForbidden)
}
}
ch, err := a.GetOrCreateDirectChannel(c, hook.UserId, result.Id)
if err != nil {
return err

Просмотреть файл

@@ -22,6 +22,37 @@ import (
"github.com/mattermost/mattermost/server/v8/channels/testlib"
)
func TestHandleIncomingWebhookDirectMessage(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
hook, appErr := th.App.CreateIncomingWebhookForChannel(th.BasicUser.Id, th.BasicChannel, &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, ChannelLocked: false})
require.Nil(t, appErr)
defer func() {
require.Nil(t, th.App.DeleteIncomingWebhook(hook.Id))
}()
t.Run("rejects DM to a user the owner shares no team with", func(t *testing.T) {
stranger := th.CreateUser()
err := th.App.HandleIncomingWebhook(th.Context, hook.Id, &model.IncomingWebhookRequest{
Text: "out of team dm",
ChannelName: "@" + stranger.Username,
})
require.NotNil(t, err)
assert.Equal(t, http.StatusForbidden, err.StatusCode)
})
t.Run("allows DM to a user the owner shares a team with", func(t *testing.T) {
err := th.App.HandleIncomingWebhook(th.Context, hook.Id, &model.IncomingWebhookRequest{
Text: "team dm",
ChannelName: "@" + th.BasicUser2.Username,
})
require.Nil(t, err)
})
}
func TestCreateIncomingWebhookForChannel(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic()