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.

criticalLuaudit check: permission-fails-open

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 pcall results. A check written as if ok and not allowed then return end lets 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, nil and 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.