How to match player names in admin commands

Partial names, display names and Luau patterns make commands hit the wrong player. How to match usernames exactly or refuse ambiguous matches.

warningLuaudit check: loose-player-matching

What it looks like

A command finds its target from typed text with a loose match: part of a name, a display name, or the text used as a Luau pattern. Short or odd input matches whoever comes first, and a moderator's ban lands on the wrong player.

Luau
if string.find(string.lower(other.Name), string.lower(text)) then
	return other
end

How to detect it

  • Find the function that turns command text into a Player and read how it compares names.
  • string.find and string.match treat the text as a pattern unless string.find gets true as its fourth argument. An empty string or . matches every name, and a stray [ or % raises an error.
  • Matching part of a name with no minimum length, or returning the first match when several players fit, picks a target the moderator didn't mean.
  • Matching on DisplayName is risky on its own. Display names aren't unique, so two players can share one, and players choose their own.
  • Words like all or others that expand to everyone deserve a look on kick, ban or data commands.

What an exploiter could do

A moderator types part of a rule-breaker's name and the command kicks or bans someone else. A player who knows how a game's commands match names can pick a display name that makes staff commands hit another player instead of them. If the text reaches a pattern, a stray symbol in a name can break the command for everyone using it.

How to fix it

  • Match the full username, ignoring case, or a UserId.
  • If you want shortcuts, compare plain prefixes (string.sub and ==, or string.find with true as the fourth argument), require a few characters, and refuse when more than one player matches.
  • Show the moderator the matched player's username, or use a list to pick from, rather than acting on a guess.

Vulnerable example

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

local function findPlayer(text: string): Player?
	for _, other in Players:GetPlayers() do
		if string.find(string.lower(other.Name), string.lower(text)) then
			return other
		end
	end
	return nil
end

return findPlayer

Fixed example

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

local function findPlayer(text: string): Player?
	local wanted = string.lower(text)
	if #wanted < 3 then
		return nil
	end
	local found: Player? = nil
	for _, other in Players:GetPlayers() do
		local name = string.lower(other.Name)
		if name == wanted then
			return other
		end
		if string.sub(name, 1, #wanted) == wanted then
			if found then
				return nil
			end
			found = other
		end
	end
	return found
end

return findPlayer

Common false positives

  • Commands that take a Player instance from an admin panel's list, rather than typed text, don't match names at all.
  • Partial matching that needs a minimum length and refuses ties is fine, even if it's looser than an exact match.
  • Matching that only feeds a harmless command, such as teleporting the caller to a friend, is low risk. Mention it rather than reporting it.