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.
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,
Teamssetup 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
ForceFieldstops damage fromHumanoid:TakeDamage, but not code that setsHealthdirectly, so useTakeDamageif 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
ForceFieldthe server gives at spawn is already server-side protection, as long as the damage goes throughTakeDamage. - Hitting a player during a round that has ended is Action not checked against the current round or state.