How to stop sword reach exploits in Roblox
A tool's handle is simulated by its holder's client, which also reports its touches. How to check reach on the server before a Touched hit deals damage.
What it looks like
A sword, bat or other melee tool deals damage from a server Touched connection on its handle or blade, and nothing checks that the victim was anywhere near the attacker. The handle is part of the attacker's character, so the attacker's own client decides when it touches something.
Luau
tool.Handle.Touched:Connect(function(hit) local humanoid = hit.Parent:FindFirstChildOfClass("Humanoid") if humanoid then humanoid:TakeDamage(15) end end)
How to detect it
- Find every server
Touchedconnection that callsTakeDamage, lowersHealth, applies knockback or credits a kill. - Work out who owns the touching part. A tool's handle or blade while equipped, a hitbox welded to a character, and any unanchored part near a player are simulated by that player's client. An anchored part, or one the server keeps with
SetNetworkOwner(nil), is the server's. - For client-owned parts, look for a server check that the victim is within reach of the attacker: the distance between the two characters'
HumanoidRootParts against the weapon's reach plus a small margin. - Swing flags and debounces are worth having, but they don't replace the distance check.
What an exploiter could do
Roblox reports touches on a part from the client that owns it, even if the server never saw the parts meet. An exploiter can have their sword "touch" players across the map, or every player at once, while swinging normally. Roblox's own documentation uses a sword dealing damage across the map as its example.
How to fix it
- In the handler, check that the victim's
HumanoidRootPartis within the weapon's reach of the attacker's, plus a small margin for latency. - Only count touches during a swing the server started, and keep a per-victim debounce for each swing.
- Better still, find hits on the server: when a swing starts, check a box in front of the attacker's character with
Workspace:GetPartBoundsInBox, or raycast from the character, instead of listening for touches. - The character's position still comes from its client, so this stops hits from places the attacker isn't, not a client that moves first. Movement checks belong to anti-exploit.
Vulnerable example
Vulnerable
local Players = game:GetService("Players") local tool = script.Parent local blade = tool:WaitForChild("Blade") :: BasePart local damage = 15 local hitThisSwing: { [Humanoid]: boolean } = {} tool.Activated:Connect(function() table.clear(hitThisSwing) end) blade.Touched:Connect(function(hit) local character = tool.Parent local owner = if character then Players:GetPlayerFromCharacter(character) else nil local model = hit:FindFirstAncestorOfClass("Model") local humanoid = model and model:FindFirstChildOfClass("Humanoid") if not owner or not humanoid or model == character or hitThisSwing[humanoid] then return end hitThisSwing[humanoid] = true humanoid:TakeDamage(damage) end)
Fixed example
Fixed
local Players = game:GetService("Players") local tool = script.Parent local blade = tool:WaitForChild("Blade") :: BasePart local damage = 15 local reach = 7 local swingTime = 0.4 local swingCooldown = 0.6 local swingStarted = -math.huge local hitThisSwing: { [Humanoid]: boolean } = {} tool.Activated:Connect(function() local now = os.clock() if now - swingStarted < swingCooldown then return end swingStarted = now table.clear(hitThisSwing) end) blade.Touched:Connect(function(hit) if os.clock() - swingStarted > swingTime then return end local character = tool.Parent local owner = if character then Players:GetPlayerFromCharacter(character) else nil local model = hit:FindFirstAncestorOfClass("Model") local humanoid = model and model:FindFirstChildOfClass("Humanoid") if not owner or not character or not model or not humanoid or model == character or humanoid.Health <= 0 or hitThisSwing[humanoid] then return end local root = character:FindFirstChild("HumanoidRootPart") local targetRoot = model:FindFirstChild("HumanoidRootPart") if not root or not root:IsA("BasePart") or not targetRoot or not targetRoot:IsA("BasePart") then return end if (root.Position - targetRoot.Position).Magnitude > reach then return end hitThisSwing[humanoid] = true humanoid:TakeDamage(damage) end)
Common false positives
- Touches on anchored parts or parts the server owns, such as lava, traps or a server-moved projectile with
SetNetworkOwner(nil), are reported by the server. - A touch that only hurts the player whose own character touched the part gives an exploiter nothing over anyone else.
- Damage to non-player targets that give nothing, such as practice dummies, can't harm anyone. A dummy that awards XP or currency still counts.