[release-10.11] MM-69010: Validate incoming webhook user membership (#36917)
Automatic Merge
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
beaa59db54
Коммит
f6d3a7827e
@@ -64,11 +64,18 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = c.App.GetUser(hook.UserId); err != nil {
|
||||
var hookUser *model.User
|
||||
if hookUser, err = c.App.GetUser(hook.UserId); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
if appErr := c.App.ValidateIncomingWebhookUser(c.AppContext, *c.AppContext.Session(), hookUser, channel); appErr != nil {
|
||||
c.LogAudit("fail - invalid webhook user")
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
|
||||
userId = hook.UserId
|
||||
}
|
||||
|
||||
@@ -162,6 +169,15 @@ func updateIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Moving the hook must not attribute its owner's posts to a channel they cannot access.
|
||||
if updatedHook.ChannelId != oldHook.ChannelId {
|
||||
if appErr := c.App.ValidateIncomingWebhookUserChannelAccess(c.AppContext, oldHook.UserId, channel); appErr != nil {
|
||||
c.LogAudit("fail - invalid webhook user")
|
||||
c.Err = appErr
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
incomingHook, err := c.App.UpdateIncomingWebhook(oldHook, &updatedHook)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
|
||||
@@ -146,6 +146,76 @@ func TestCreateIncomingWebhook_BypassTeamPermissions(t *testing.T) {
|
||||
CheckForbiddenStatus(t, resp)
|
||||
}
|
||||
|
||||
func TestIncomingWebhookValidateUser(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableIncomingWebhooks = true })
|
||||
|
||||
defaultRolePermissions := th.SaveDefaultRolePermissions()
|
||||
defer th.RestoreDefaultRolePermissions(defaultRolePermissions)
|
||||
th.AddPermissionToRole(model.PermissionManageIncomingWebhooks.Id, model.TeamAdminRoleId)
|
||||
|
||||
th.LoginTeamAdmin()
|
||||
|
||||
t.Run("cannot assign a user who is not a member of the team or channel", func(t *testing.T) {
|
||||
nonMember := th.CreateUser()
|
||||
|
||||
hook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, UserId: nonMember.Id}
|
||||
_, resp, err := th.Client.CreateIncomingWebhook(context.Background(), hook)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("cannot assign a user with higher privileges than the requester", func(t *testing.T) {
|
||||
th.LinkUserToTeam(th.SystemAdminUser, th.BasicTeam)
|
||||
_, appErr := th.App.AddUserToChannel(th.Context, th.SystemAdminUser, th.BasicChannel, false)
|
||||
require.Nil(t, appErr)
|
||||
|
||||
hook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, UserId: th.SystemAdminUser.Id}
|
||||
_, resp, err := th.Client.CreateIncomingWebhook(context.Background(), hook)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("can assign a user who is a member of the channel", func(t *testing.T) {
|
||||
hook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, UserId: th.BasicUser2.Id}
|
||||
created, _, err := th.Client.CreateIncomingWebhook(context.Background(), hook)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, th.BasicUser2.Id, created.UserId)
|
||||
})
|
||||
|
||||
t.Run("update cannot move another user's hook to a channel they cannot access", func(t *testing.T) {
|
||||
hook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, UserId: th.BasicUser2.Id}
|
||||
created, _, err := th.Client.CreateIncomingWebhook(context.Background(), hook)
|
||||
require.NoError(t, err)
|
||||
|
||||
privateChannel := th.CreatePrivateChannel()
|
||||
created.ChannelId = privateChannel.Id
|
||||
|
||||
_, resp, err := th.Client.UpdateIncomingWebhook(context.Background(), created)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
|
||||
t.Run("update validates the retained owner even when the payload also changes the owner", func(t *testing.T) {
|
||||
hook := &model.IncomingWebhook{ChannelId: th.BasicChannel.Id, UserId: th.BasicUser2.Id}
|
||||
created, _, err := th.Client.CreateIncomingWebhook(context.Background(), hook)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The owner is immutable on update, so changing it alongside the channel must not
|
||||
// let the supplied user stand in for the retained owner's channel access.
|
||||
privateChannel := th.CreatePrivateChannel()
|
||||
created.ChannelId = privateChannel.Id
|
||||
created.UserId = th.TeamAdminUser.Id
|
||||
|
||||
_, resp, err := th.Client.UpdateIncomingWebhook(context.Background(), created)
|
||||
require.Error(t, err)
|
||||
CheckForbiddenStatus(t, resp)
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetIncomingWebhooks(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -4554,6 +4554,14 @@
|
||||
"id": "api.webhook.create_outgoing.triggers.app_error",
|
||||
"translation": "Either trigger_words or channel_id must be set."
|
||||
},
|
||||
{
|
||||
"id": "api.webhook.incoming.user_membership.app_error",
|
||||
"translation": "The webhook user must be a member of the target team or channel."
|
||||
},
|
||||
{
|
||||
"id": "api.webhook.incoming.user_role.app_error",
|
||||
"translation": "You cannot assign a webhook to a user with higher privileges than your own."
|
||||
},
|
||||
{
|
||||
"id": "api.webhook.team_mismatch.app_error",
|
||||
"translation": "Unable to update webhook across teams."
|
||||
|
||||
Ссылка в новой задаче
Block a user