How to stop players getting others kicked or banned

Report buttons, vote-kicks and client anti-cheat that let one client punish another. How to keep punishment decisions on the server or with moderators.

criticalLuaudit check: client-triggered-punishment

What it looks like

A remote lets a client name a player, and the server kicks, bans, kills, freezes or flags that player without checking anything itself. It's often part of a report button, a vote-kick, or anti-cheat that runs on each client and reports other players it thinks are cheating.

Luau
remote.OnServerEvent:Connect(function(player, cheater)
	cheater:Kick("Exploiting")
end)

How to detect it

  • Find every server call to Kick, every write to a ban list or ban data store, and every punishment such as killing, freezing, muting or resetting data.
  • For each one, trace who the target is. If a client argument picks the target, check what the server verifies before acting.
  • A report that only gets logged for a human moderator is fine. A report that acts straight away on one client's word is a finding.
  • For vote-kicks, check that the server counts votes per voter, needs a real share of the server and limits how often a vote can start.

What an exploiter could do

They can kick, ban or punish any player they like, as often as they like, including the game's own staff. On a game with a saved ban list, that harm lasts until someone notices and undoes it by hand.

How to fix it

  • Never punish on one client's word. Decide on the server, from what the server itself can see.
  • Turn reports into a log entry or a message for moderators, with a per-player cooldown, and leave the decision to a person or to server-side checks.
  • If you keep vote-kicks, count one vote per player, require a majority of the server, add a cooldown and don't let a vote ban anyone permanently.

Vulnerable example

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

ReplicatedStorage.Remotes.ReportCheater.OnServerEvent:Connect(function(player: Player, suspect: unknown)
	if typeof(suspect) ~= "Instance" or not suspect:IsA("Player") or suspect == player then
		return
	end
	if suspect.Parent == Players then
		suspect:Kick("You were reported for exploiting.")
	end
end)

Fixed example

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

local cooldown = 60
local lastReport: { [Player]: number } = {}

ReplicatedStorage.Remotes.ReportCheater.OnServerEvent:Connect(function(player: Player, suspect: unknown)
	if typeof(suspect) ~= "Instance" or not suspect:IsA("Player") or suspect == player then
		return
	end
	local now = os.clock()
	if now - (lastReport[player] or 0) < cooldown then
		return
	end
	lastReport[player] = now
	print(HttpService:JSONEncode({
		reporter = player.UserId,
		suspect = suspect.UserId,
		time = os.time(),
	}))
end)

Players.PlayerRemoving:Connect(function(player)
	lastReport[player] = nil
end)

Common false positives

  • Kicks the server decides on its own, such as a server-side speed check, aren't client-triggered.
  • Admin commands are covered by Admin powers not checked on the server. Report a moderation remote there if it's meant for staff only.
  • A client kicking itself, with LocalPlayer:Kick(), only affects that player.