Why removed staff keep their powers until they leave
Group rank and role lookups are cached for the session. How to revoke staff powers straight away with a revocation list.
What it looks like
Staff powers come from the player's rank in a Roblox group. The server checks GroupService:GetRolesInGroupAsync, or the Player methods GetRankInGroupAsync, GetRoleInGroupAsync or IsInGroupAsync, and treats the answer as current for the whole session.
local function isModerator(player) return player:GetRankInGroupAsync(staffGroup) >= 200 end
How to detect it
- Find every group check that grants powers: admin commands, moderation panels, staff-only areas and tools.
- Roblox's reference for the
Playermethods says they cache their results: a player who leaves the group during the session is still treated as a member, at their old rank, until they leave the experience.GroupService:GetRolesInGroupAsync, which supersedesGetRankInGroupAsyncandGetRoleInGroupAsync, is also cached per user and group and may return an old answer until the cache expires. - The reference for
GetRolesInGroupAsyncalso says a role'sRankno longer decides its place in the hierarchy, and to identify roles by theirId. A check likeRank >= 200may not mean what it used to. Mention it, as advice. - Work out how long a server can run with a demoted member in it. A moderator removed for abuse keeps every power until they choose to leave, and servers can run for hours or days.
- Look for a faster way to take powers away: a ban or block list in a data store, a
MessagingServicetopic that tells servers to revoke someone, or a kick on demotion. - Check that the highest powers, such as bans, data edits, currency and shutdowns, don't rest on a group rank alone. A
UserIdlist for those is easier to control.
What an exploiter could do
A moderator who's been demoted or removed, or someone using a stolen moderator account that has been taken out of the group, stays in their current server and keeps using staff commands. They can ban players, change data or give out currency, and nothing stops them until they leave.
How to fix it
- Keep a revocation list in a data store, check it in every staff command, and publish changes over
MessagingServiceso servers update straight away. - Kick a player from all servers when they are removed from staff.
GetRolesInGroupAsyncmay still return its cached answer when they rejoin, so check the revocation list on join as well. - Gate the most dangerous commands on a server-side
UserIdlist as well as the group rank.
Vulnerable example
local GroupService = game:GetService("GroupService") local ReplicatedStorage = game:GetService("ReplicatedStorage") local staffGroup = 9100300 local moderatorRoles: { [number]: boolean } = { [51001] = true, [51002] = true, } local function isModerator(player: Player): boolean local ok, info = pcall(GroupService.GetRolesInGroupAsync, GroupService, player.UserId, staffGroup) if not ok or typeof(info) ~= "table" then return false end for _, role in info.Roles do if moderatorRoles[role.Id] then return true end end return false end ReplicatedStorage.Remotes.StaffKick.OnServerEvent:Connect(function(player: Player, target: unknown) if not isModerator(player) then return end if typeof(target) == "Instance" and target:IsA("Player") and target ~= player then target:Kick("Removed by a moderator.") end end)
Fixed example
local GroupService = game:GetService("GroupService") local MessagingService = game:GetService("MessagingService") local ReplicatedStorage = game:GetService("ReplicatedStorage") local staffGroup = 9100300 local moderatorRoles: { [number]: boolean } = { [51001] = true, [51002] = true, } local revoked: { [number]: boolean } = {} pcall(MessagingService.SubscribeAsync, MessagingService, "RevokeStaff", function(message) if typeof(message.Data) == "number" then revoked[message.Data] = true end end) local function isModerator(player: Player): boolean if revoked[player.UserId] then return false end local ok, info = pcall(GroupService.GetRolesInGroupAsync, GroupService, player.UserId, staffGroup) if not ok or typeof(info) ~= "table" then return false end for _, role in info.Roles do if moderatorRoles[role.Id] then return true end end return false end ReplicatedStorage.Remotes.StaffKick.OnServerEvent:Connect(function(player: Player, target: unknown) if not isModerator(player) then return end if typeof(target) == "Instance" and target:IsA("Player") and target ~= player then target:Kick("Removed by a moderator.") end end)
A real revocation list should also be saved in a data store and loaded when the server starts, so servers that missed the message still apply it.
Common false positives
- Group checks that only unlock cosmetics, such as a group-member chat tag or a door to a lounge, aren't worth reporting.
- Games where staff powers come from a server-side
UserIdlist, or that read a revocation list on every command, are fine. - If the check also fails open when the lookup errors, report Permission check that lets people in when it fails too.