How to secure admin commands and admin remotes

Admin panels that are only hidden on the client leave their remotes open to everyone. How to check the caller on the server in every admin handler.

criticalLuaudit check: unguarded-admin-remotes

What it looks like

A remote runs an admin or developer command, such as giving currency, kicking, teleporting, spawning items or changing the round, and the server doesn't check that the caller is allowed. Sometimes the only check is in the admin panel's LocalScript, or the server checks something the client controls.

Luau
remote.OnServerEvent:Connect(function(player, target, amount)
	giveCoins(target, amount)
end)

How to detect it

  • Find remotes whose handlers do something only staff should do: grant currency or items, kick or ban, teleport other players, change other players' data, run commands, or change server settings.
  • Look for a permission check inside the handler, on the server, before anything happens. Hiding the admin GUI from other players doesn't count, since anyone can fire the remote.
  • Check what the permission check uses. It should be a list of UserIds kept on the server, a group rank checked on the server, or the place's creator. A check on player.Name can break when names change, and a check on an attribute, value or argument the client set or sent isn't a check at all.
  • Admin lists stored in ReplicatedStorage can be read by exploiters. That isn't a hole on its own, but the server must still do the check.
  • Test or debug remotes left in the live game, such as "GiveMeCoins", count even if no GUI uses them.

What an exploiter could do

Whatever the admin command can do, they can do: give themselves unlimited currency, kick or ban other players, teleport everyone or wreck the server for everyone in it.

How to fix it

  • At the top of every admin handler, check the caller against a server-side list of UserIds, or a group rank checked on the server, and return before doing anything if they aren't on it.
  • Keep the admin remotes and their handlers in one place so none is missed, and remove test remotes from the live game.
  • Validate admin arguments too. A staff member's account can be compromised, and a typo shouldn't wipe a player's data.

Vulnerable example

Vulnerable
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlayerData = require(script.Parent.PlayerData)

ReplicatedStorage.Remotes.AdminGiveCoins.OnServerEvent:Connect(function(player: Player, target: unknown, amount: unknown)
	if typeof(target) ~= "Instance" or not target:IsA("Player") then
		return
	end
	if typeof(amount) ~= "number" or not (amount >= 1 and amount <= 100000 and amount % 1 == 0) then
		return
	end
	local profile = PlayerData.get(target)
	if profile then
		profile.Coins += amount
	end
end)

Fixed example

Fixed
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local PlayerData = require(script.Parent.PlayerData)

local admins: { [number]: boolean } = {
	[123456789] = true,
}

ReplicatedStorage.Remotes.AdminGiveCoins.OnServerEvent:Connect(function(player: Player, target: unknown, amount: unknown)
	if not admins[player.UserId] then
		return
	end
	if typeof(target) ~= "Instance" or not target:IsA("Player") then
		return
	end
	if typeof(amount) ~= "number" or not (amount >= 1 and amount <= 100000 and amount % 1 == 0) then
		return
	end
	local profile = PlayerData.get(target)
	if profile then
		profile.Coins += amount
	end
end)

Common false positives

  • Admin systems from well-known admin modules check permissions inside the module. Confirm the game uses the module's own command system rather than its own remotes.
  • Commands that only affect the caller and give nothing, like toggling their own fly camera in a building game, may be fine for everyone by design.
  • Studio-only code guarded by RunService:IsStudio() doesn't run in live servers.