How to secure admin chat commands in Roblox
A hidden TextChatCommand alias isn't a permission check. How to check the speaker's UserId or group role on the server before a command runs.
What it looks like
A staff or developer command is run from chat, through TextChatCommand.Triggered or Player.Chatted, and the server handler never checks who sent it. The command is often hidden from autocomplete or has an unusual alias, as if nobody else would think to type it.
Luau
local giveCommand = TextChatService:WaitForChild("GiveCoins") :: TextChatCommand giveCommand.Triggered:Connect(function(textSource, message) giveCoins(textSource.UserId, tonumber(string.split(message, " ")[2])) end)
How to detect it
- Find every
TextChatCommandinTextChatService, including ones created by scripts, and everyTriggeredconnection. Find everyPlayer.Chattedconnection that parses commands. - Work out what each command does. Commands that only affect the speaker's own look, like a size or emote command, are fine. Commands that grant currency or items, change other players, kick, ban, teleport, change the round or touch saved data need a permission check.
- In each
Triggeredhandler, check that the speaker is looked up fromtextSource.UserIdand compared with a server-side list ofUserIds or a group role checked on the server, before anything happens. - A command handled only in a LocalScript, which then fires a remote, moves the check to that remote. Judge the remote with Admin powers not checked on the server.
AutocompleteVisible = false, a secret alias or a long password-like command isn't a permission check. Anyone who learns the alias, from a leaked place file, a video or by reading client scripts that mention it, can type it.
What an exploiter could do
They don't even need an exploit. Anyone who types the command gets what it does: unlimited currency or items, kicking or banning other players, teleporting everyone or ending rounds. Command names spread quickly once one player finds them.
How to fix it
- At the top of every staff command's handler, find the player with
Players:GetPlayerByUserId(textSource.UserId)and return unless theirUserIdis on the server's staff list or their group role passes a server check. - Keep staff-only commands in one script so none is missed.
- Validate every argument parsed from the message, as Command numbers not range-checked and Command targets matched too loosely describe.
Vulnerable example
Vulnerable
local Players = game:GetService("Players") local TextChatService = game:GetService("TextChatService") local PlayerData = require(script.Parent.PlayerData) local command = Instance.new("TextChatCommand") command.Name = "GiveCoins" command.PrimaryAlias = "/coins" command.AutocompleteVisible = false command.Parent = TextChatService command.Triggered:Connect(function(textSource: TextSource, message: string) local amount = tonumber(string.match(message, "^/coins (%d+)$")) local player = Players:GetPlayerByUserId(textSource.UserId) local profile = player and PlayerData.get(player) if profile and amount and amount <= 100000 then profile.Coins += amount end end)
Fixed example
Fixed
local Players = game:GetService("Players") local TextChatService = game:GetService("TextChatService") local PlayerData = require(script.Parent.PlayerData) local staff: { [number]: boolean } = { [123456789] = true, [987654321] = true, } local command = Instance.new("TextChatCommand") command.Name = "GiveCoins" command.PrimaryAlias = "/coins" command.AutocompleteVisible = false command.Parent = TextChatService command.Triggered:Connect(function(textSource: TextSource, message: string) if not staff[textSource.UserId] then return end local amount = tonumber(string.match(message, "^/coins (%d+)$")) local player = Players:GetPlayerByUserId(textSource.UserId) local profile = player and PlayerData.get(player) if profile and amount and amount <= 100000 then profile.Coins += amount end end)
Common false positives
- Commands everyone is meant to use, such as
/help,/emoteor changing their own settings, need no permission check. - Roblox's built-in commands, made when
CreateDefaultCommandsis on, are handled by Roblox. - A command that checks the speaker's
UserIdor group role on the server, like the fixed example, is fine even if the check sits in a helper function. Follow the call.