[release-10.11] MM-69010: Validate incoming webhook user membership (#36917)
Automatic Merge
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
beaa59db54
Коммит
f6d3a7827e
@@ -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()
|
||||
|
||||
Ссылка в новой задаче
Block a user