How to enforce teams, duels and safe zones on the server

Friendly fire, duel opponents and safe zones checked only on the client let exploiters hit anyone. How to check who may be attacked in every damage handler.

criticalLuaudit check: unchecked-targets

What it looks like

The game has rules about who can be hit: teams, duels, a PvP toggle, a lobby or safe zone, spawn protection. The client follows them, by not targeting teammates or not letting you swing in the lobby, but the server's damage code never checks them.

Luau
remote.OnServerEvent:Connect(function(player, victim)
	if inRange(player, victim) then
		victim.Humanoid:TakeDamage(20)
	end
end)

How to detect it

  • Work out the game's targeting rules from its client scripts, UI, Teams setup and settings: friendly fire, duel or match opponents, PvP opt-in, safe zones, spawn protection, spectators.
  • For each server handler that damages, stuns, knocks back or eliminates another player, check that every rule is enforced there, using state the server keeps: Player.Team, the server's record of who is duelling whom, a PvP setting stored on the server, a safe-zone check against the victim's position, or a protection time recorded at spawn.
  • A rule only the client applies, such as a LocalScript that filters out teammates before firing the remote, is a finding.
  • Check both sides where it matters: a player in a safe zone shouldn't be hit, and usually shouldn't be able to attack from it either.

What an exploiter could do

They can attack players the game promises are safe: teammates, people who turned PvP off, players in the lobby, players outside their duel, or players who've just spawned. That spoils the game for everyone they pick on, and in team modes it can decide the round.

How to fix it

  • Keep the rules on the server and check them in every damage handler, ideally in one shared function that every weapon calls.
  • Read team and duel state from the server's own records, never from the client's arguments.
  • For safe zones, check the victim's and attacker's positions against the zone on the server, or keep a "protected" flag the server sets when players enter and leave.
  • Roblox's ForceField stops damage from Humanoid:TakeDamage, but not code that sets Health directly, so use TakeDamage if you rely on one for spawn protection.

Vulnerable example

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

local damage = 20
local reach = 8
local cooldown = 0.6
local lastHit: { [Player]: number } = {}

ReplicatedStorage.Remotes.Punch.OnServerEvent:Connect(function(player: Player, victim: unknown)
	if typeof(victim) ~= "Instance" or not victim:IsA("Player") or victim == player then
		return
	end
	local character = player.Character
	local target = victim.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	local targetRoot = target and target:FindFirstChild("HumanoidRootPart")
	local humanoid = target and target:FindFirstChildOfClass("Humanoid")
	if not root or not root:IsA("BasePart") or not targetRoot or not targetRoot:IsA("BasePart") or not humanoid then
		return
	end
	if (root.Position - targetRoot.Position).Magnitude > reach then
		return
	end
	local now = os.clock()
	if now - (lastHit[player] or -math.huge) < cooldown then
		return
	end
	lastHit[player] = now
	humanoid:TakeDamage(damage)
end)

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

Fixed example

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

local damage = 20
local reach = 8
local cooldown = 0.6
local lastHit: { [Player]: number } = {}

local function canAttack(attacker: Player, victim: Player): boolean
	if attacker.Neutral or victim.Neutral then
		return true
	end
	return attacker.Team ~= victim.Team
end

ReplicatedStorage.Remotes.Punch.OnServerEvent:Connect(function(player: Player, victim: unknown)
	if typeof(victim) ~= "Instance" or not victim:IsA("Player") or victim == player then
		return
	end
	if not canAttack(player, victim) then
		return
	end
	local character = player.Character
	local target = victim.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	local targetRoot = target and target:FindFirstChild("HumanoidRootPart")
	local humanoid = target and target:FindFirstChildOfClass("Humanoid")
	if not root or not root:IsA("BasePart") or not targetRoot or not targetRoot:IsA("BasePart") or not humanoid or humanoid.Health <= 0 then
		return
	end
	if (root.Position - targetRoot.Position).Magnitude > reach then
		return
	end
	local now = os.clock()
	if now - (lastHit[player] or -math.huge) < cooldown then
		return
	end
	lastHit[player] = now
	humanoid:TakeDamage(damage)
end)

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

Common false positives

  • Free-for-all games with no teams, duels, safe zones or PvP setting have no rule to check.
  • Team checks done by a shared damage module that every weapon calls count for every weapon that uses it.
  • Damage blocked by a ForceField the server gives at spawn is already server-side protection, as long as the damage goes through TakeDamage.
  • Hitting a player during a round that has ended is Action not checked against the current round or state.