Why admin checks must fail closed
A group role or staff list lookup that errors can let anyone run staff commands. How to treat failures, missing data and unfinished loads as no permission.
What it looks like
The server looks up whether a player is staff with a call that can fail, such as GroupService:GetRolesInGroupAsync, a data store or a web request, and a failure lets the command run. Often the check only refuses when the lookup worked and said no.
Luau
local ok, membership = pcall(GroupService.GetRolesInGroupAsync, GroupService, player.UserId, groupId) if ok and not hasStaffRole(membership) then return end
How to detect it
- Find every staff or permission lookup and ask what happens when it errors, returns
nil, is still loading, or doesn't find the player. Each of those must end in "not allowed". - Look at
pcallresults. A check written asif ok and not allowed then return endlets every failure through. - Look for defaults that give power:
or 255,or true, a missing entry treated as allowed, or a deny list where anyone not listed is let in. - Staff lists loaded at startup from a data store or the web often have a window, before the load finishes or after it fails, where the check is skipped.
- Check results kept in a cache. A failed lookup shouldn't be saved as if it were an answer, in either direction.
What an exploiter could do
Group, data store and web requests fail during Roblox service problems and when the server runs out of request budget. While they do, anyone who runs a command gets staff powers, which can mean currency, kicks, bans or shutting the server down. A deny list doesn't need a failure at all: it lets everyone in from the start.
How to fix it
- Start from "not allowed" and only grant access when a lookup succeeds and says yes.
- Treat errors,
niland unfinished loads as no permission. Retry the lookup later rather than guessing. - Use an allow list of staff, never a list of who isn't allowed.
- Cache only successful answers.
Vulnerable example
Vulnerable
local GroupService = game:GetService("GroupService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local groupId = 1234567 local staffRoles: { [number]: boolean } = { [41000001] = true, [41000002] = true, } local function hasStaffRole(membership: any): boolean for _, role in membership.Roles do if staffRoles[role.Id] then return true end end return false end ReplicatedStorage.Remotes.RestartServer.OnServerEvent:Connect(function(player: Player) local ok, membership = pcall(GroupService.GetRolesInGroupAsync, GroupService, player.UserId, groupId) if ok and not hasStaffRole(membership) then return end for _, other in Players:GetPlayers() do other:Kick("This server is restarting.") end end)
Fixed example
Fixed
local GroupService = game:GetService("GroupService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local groupId = 1234567 local staffRoles: { [number]: boolean } = { [41000001] = true, [41000002] = true, } local known: { [Player]: boolean } = {} local function isStaff(player: Player): boolean local cached = known[player] if cached ~= nil then return cached end local ok, result = pcall(function() local membership = GroupService:GetRolesInGroupAsync(player.UserId, groupId) for _, role in membership.Roles do if staffRoles[role.Id] then return true end end return false end) if not ok then return false end known[player] = result return result end ReplicatedStorage.Remotes.RestartServer.OnServerEvent:Connect(function(player: Player) if not isStaff(player) then return end for _, other in Players:GetPlayers() do other:Kick("This server is restarting.") end end) Players.PlayerRemoving:Connect(function(player) known[player] = nil end)
Common false positives
- A lookup that fails and only removes access, such as hiding a staff badge, isn't a hole. The problem is only when a failure grants something.
- A hardcoded table of
UserIds can't fail, so there's nothing to check beyond the table itself. - Well-known admin modules handle failed lookups themselves. Report them only if the game's own code around them adds a fallback that grants access.