From dff4a25332a783ca49a9ef66b9579e8bb12a1d0a Mon Sep 17 00:00:00 2001 From: grendervill Date: Sun, 20 Sep 2026 22:57:45 +0300 Subject: [PATCH] =?UTF-8?q?fix(moderation):=20=D0=B0=D0=B4=D0=BC=D0=B8?= =?UTF-8?q?=D0=BD=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=82=D0=BE=D1=80=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=81=D1=82=D0=B0=D0=BD=D1=81=D0=B0=20=D1=82=D0=BE=D0=B6?= =?UTF-8?q?=D0=B5=20=D0=BD=D0=B5=20=D0=BC=D0=BE=D0=B6=D0=B5=D1=82=20=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B5=D1=80=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D1=8C=20=D1=81=D0=B5=D0=B1=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проверка иерархии пропускала все запреты для администратора инстанса, включая запрет на модерацию себя: он мог забанить или исключить самого себя и потерять доступ к серверу (поймано при проверке интерфейса на стенде — самобан снял участие в «Главном сервере»). Теперь проверка `targetID == actor.ID` выполняется до обхода иерархии, для администратора инстанса остаются доступны любые другие цели. Регрессионный тест: самобан и самоисключение администратора инстанса дают 403. --- internal/server/api_guilds.go | 8 +++++--- internal/server/moderation_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/internal/server/api_guilds.go b/internal/server/api_guilds.go index 78572ce..12a2f54 100644 --- a/internal/server/api_guilds.go +++ b/internal/server/api_guilds.go @@ -974,6 +974,11 @@ func (s *Server) changeMemberRole(ctx context.Context, guildIDRaw, userIDRaw, ro // checkMemberHierarchy запрещает действовать против владельца и участников с // более высокими ролями (AGENT.md 6.3, 7.17). func (s *Server) checkMemberHierarchy(ctx context.Context, guildID uint64, actor *store.User, resolved permissions.Resolved, targetID uint64) error { + // Модерировать себя нельзя никому, включая администратора инстанса: иначе + // он может случайно исключить или забанить сам себя (проверено на стенде). + if targetID == actor.ID { + return humaErrorStatus(http.StatusForbidden, "perm.denied", "you cannot moderate yourself") + } if actor.IsInstanceAdmin { return nil } @@ -989,9 +994,6 @@ func (s *Server) checkMemberHierarchy(ctx context.Context, guildID uint64, actor if targetID == guild.OwnerID && actor.ID != guild.OwnerID { return humaErrorStatus(http.StatusForbidden, "perm.denied", "guild owner cannot be moderated") } - if targetID == actor.ID { - return humaErrorStatus(http.StatusForbidden, "perm.denied", "you cannot moderate yourself") - } actorRoles, err := s.roleData(ctx, guildID, actor.ID) if err != nil { return err diff --git a/internal/server/moderation_test.go b/internal/server/moderation_test.go index db15a96..8a04379 100644 --- a/internal/server/moderation_test.go +++ b/internal/server/moderation_test.go @@ -209,6 +209,30 @@ func TestMemberTimeoutBlocksSending(t *testing.T) { } } +// TestInstanceAdminCannotModerateSelf: администратор инстанса обходит иерархию +// ролей, но не запрет на модерацию себя — иначе он исключает себя из сервера. +func TestInstanceAdminCannotModerateSelf(t *testing.T) { + srv, owner, _, guildID, _ := banFixture(t) + promoteAdmin(t, srv, "ban-owner@example.com") + + self := doJSON(t, srv, http.MethodGet, "/api/v1/users/@me", "", owner) + ownerID := decodeResponse[struct { + User struct { + ID string `json:"id"` + } `json:"user"` + }](t, self).User.ID + + ban := doJSON(t, srv, http.MethodPut, "/api/v1/guilds/"+guildID+"/bans/"+ownerID, + `{"reason":"сам себя"}`, owner) + if ban.Code != http.StatusForbidden { + t.Fatalf("самобан администратора инстанса = %d, ожидался 403", ban.Code) + } + kick := doJSON(t, srv, http.MethodDelete, "/api/v1/guilds/"+guildID+"/members/"+ownerID, "", owner) + if kick.Code != http.StatusForbidden { + t.Fatalf("самоисключение администратора инстанса = %d, ожидался 403", kick.Code) + } +} + func TestTimeoutCannotBeSetOnSelf(t *testing.T) { srv, owner, _, guildID, _ := banFixture(t) // участник не нужен: проверяем себя self := doJSON(t, srv, http.MethodGet, "/api/v1/users/@me", "", owner)