How to limit Roblox admin commands by staff rank
When every staff member can run every command, a trial moderator can ban the owner. How to give each command a level and stop staff acting on higher ranks.
What it looks like
The game has more than one staff rank, such as moderator, admin and owner, but the server only asks whether the caller is staff. Every rank can run every command. Or each command has a rank, but nothing compares the caller with the player they're acting on, so a moderator can ban an admin or the owner.
Luau
if staff[player.UserId] then commands[name](player, target) end
How to detect it
- Find where the game decides who is staff: a
UserIdtable, a group rank, a data store list or a command module's settings. Note how many levels it has. If several scripts keep their own staff tables, compare them: a level set in one script means little if another script treats the same accounts as equals. - For each command, find the level it needs and check that the server compares the caller's level with it before running anything. A rank shown in the admin panel, or commands hidden from lower ranks in the UI, doesn't count.
- For every command that acts on another player, such as kick, ban, mute, jail, teleport, or changing their data or rank, check that it refuses targets at the caller's level or above.
- Commands that change ranks need both checks: the caller can't give a rank at or above their own, and can't change the rank of someone at or above theirs.
- Look for server-wide commands, like shutdown, global announcements, giving currency or wiping data, that are open to the lowest rank.
What an exploiter could do
Anyone with the lowest staff rank, including a trial moderator or someone using a stolen moderator account, can use the strongest commands: give themselves currency, shut servers down, or ban the owner and every other member of staff so nobody can undo it.
How to fix it
- Give every command a minimum level, kept on the server next to the command itself.
- Look up the caller's level on the server and refuse the command if it's below the command's level.
- For commands on another player, refuse when the target's level is the same as or higher than the caller's.
- Keep one table of commands and one place that checks it, so a new command can't skip the check.
Vulnerable example
Vulnerable
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local levels: { [number]: number } = { [123456789] = 3, [987654321] = 1, } local commands: { [string]: (Player) -> () } = { kick = function(target) target:Kick("Removed by staff.") end, ban = function(target) local ok, err = pcall(Players.BanAsync, Players, { UserIds = { target.UserId }, Duration = -1, DisplayReason = "Banned by staff.", PrivateReason = "Staff command", }) if not ok then warn(err) end end, } ReplicatedStorage.Remotes.StaffCommand.OnServerEvent:Connect(function(player: Player, name: unknown, target: unknown) if not levels[player.UserId] then return end if typeof(name) ~= "string" or typeof(target) ~= "Instance" or not target:IsA("Player") then return end local command = commands[name] if command then command(target) end end)
Fixed example
Fixed
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") type Command = { level: number, run: (Player) -> (), } local levels: { [number]: number } = { [123456789] = 3, [987654321] = 1, } local commands: { [string]: Command } = { kick = { level = 1, run = function(target) target:Kick("Removed by staff.") end, }, ban = { level = 2, run = function(target) local ok, err = pcall(Players.BanAsync, Players, { UserIds = { target.UserId }, Duration = -1, DisplayReason = "Banned by staff.", PrivateReason = "Staff command", }) if not ok then warn(err) end end, }, } ReplicatedStorage.Remotes.StaffCommand.OnServerEvent:Connect(function(player: Player, name: unknown, target: unknown) local level = levels[player.UserId] or 0 if typeof(name) ~= "string" or typeof(target) ~= "Instance" or not target:IsA("Player") then return end local command = commands[name] if not command or level < command.level then return end if (levels[target.UserId] or 0) >= level then return end command.run(target) end)
Common false positives
- Games with a single staff level, where everyone on the list is trusted equally, don't need per-command levels. Mention it only if the list mixes trusted and trial staff.
- Well-known admin modules check ranks inside the module. Check the ranks given in its settings rather than reporting the module itself.
- Commands that only affect the caller, like flying or going invisible to test a map, don't need a target check.