How to find hidden admin backdoors in your game

Free models that give commands to their maker's UserId or group, or add a second handler to your staff remotes. How to find them and remove them.

criticalLuaudit check: hidden-admin-grants

What it looks like

A script gives commands, tools or staff powers to a UserId, name or group that isn't on the game's own staff list. It's often in a free model, labelled as credits for its maker, or tucked into a second handler on one of the game's own remotes.

Luau
if player.UserId == 31415926 then
	player.Chatted:Connect(runCommand)
end

How to detect it

  • List every hardcoded UserId, player name and group id in server code, including checks through IsInGroupAsync, GetRankInGroupAsync and GetRoleInGroupAsync, or the older versions without Async. Ids may be hidden too, built from string.char lists, tonumber on joined strings or arithmetic, which is also Hidden or scrambled code. Compare each one with the game's own staff list and the group that owns the game. Any that isn't on it needs a reason.
  • Look at what each match unlocks: chat commands through Chatted, TextChatCommand or TextChatService callbacks, admin GUIs, tools cloned from storage, currency, kicks or bans, or a place on an admin list.
  • Look for more than one OnServerEvent connection on the same remote, especially staff remotes. A second handler with its own permission check is a common way to slip extra users in.
  • Look for checks against game.CreatorId. In a group game it's the group's id, not a person's, so player.UserId == game.CreatorId lets in whichever account happens to have that number. Check game.CreatorType first, or use the staff list.

What an exploiter could do

Whoever the script lets in can use the powers on any server of the game: kicking or banning players, giving themselves currency, running commands or shutting servers down. Because the check names them on purpose, nothing in the game's own admin system shows them as staff.

How to fix it

  • Remove the grant and the script it came in. If it's part of a free model, replace the model.
  • Keep one staff list on the server, and give every staff power through it, with one handler per staff remote.
  • Search the game for the same ids. Backdoors are often copied into several scripts.

Vulnerable example

Vulnerable
local Players = game:GetService("Players")

local modelCredits = 31415926

Players.PlayerAdded:Connect(function(player)
	if player.UserId ~= modelCredits then
		return
	end
	player.Chatted:Connect(function(message)
		local name = string.match(message, "^;kick ([%w_]+)$")
		local target = name and Players:FindFirstChild(name)
		if target and target:IsA("Player") then
			target:Kick()
		end
	end)
end)

Fixed example

Fixed
local Players = game:GetService("Players")

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

local function listen(player: Player)
	if not staff[player.UserId] then
		return
	end
	player.Chatted:Connect(function(message)
		local name = string.match(message, "^;kick ([%w_]+)$")
		local target = name and Players:FindFirstChild(name)
		if target and target:IsA("Player") and target ~= player then
			target:Kick("Removed by a moderator.")
		end
	end)
end

Players.PlayerAdded:Connect(listen)
for _, player in Players:GetPlayers() do
	listen(player)
end

Common false positives

  • UserIds or group ranks that match the game's own staff, owner or owning group are normal. Ask the user who their staff are if the code doesn't say.
  • Ids used only for something cosmetic, like a name tag or a thank-you message, give nothing away. Mention them only if they come from a free model.
  • Well-known admin modules read their admin list from settings the game owner fills in. Check those settings rather than reporting the module.