How to secure RemoteEvents inside tools
A remote inside an equipped tool can be fired by any client. How to check the caller is the tool's holder before acting.
What it looks like
A tool carries its own RemoteEvent or RemoteFunction, and the server handler acts for whoever is holding the tool without checking that the player who fired the remote is that person. Often the handler ignores its first argument, or names it _.
Luau
tool.Fire.OnServerEvent:Connect(function(_, target) local holder = Players:GetPlayerFromCharacter(tool.Parent) damage(holder, target) end)
How to detect it
- Find remotes that live inside tools, or are created per tool or per player, and their server handlers.
- In each handler, check that the player who fired it is the tool's holder:
tool.Parent == player.Character, ortool:IsDescendantOf(player)for an unequipped tool where using it from the backpack is allowed. - A handler that ignores the
playerargument and works out the attacker fromtool.Parent, the tool's owner attribute or a variable captured when the tool was handed out is a finding. - The same applies to shared remotes that take a tool as an argument. That shape is also Instance arguments used without an ownership check; report it once, here.
What an exploiter could do
While a tool is equipped it sits in its holder's character in Workspace, where every client can see it and its remotes. An exploiter can fire other players' weapons and abilities: dealing damage from their positions, using them faster than any one player could, and getting other players blamed or punished for attacks they never made.
How to fix it
- In every tool handler, check the caller is the holder before anything else, and use
player, nevertool.Parent, as the attacker. - Check the tool is equipped, not just owned, if the action needs it in hand.
- Keep cooldowns per player on the server so firing through several tools doesn't add up.
Vulnerable example
Vulnerable
local Players = game:GetService("Players") local tool = script.Parent local throw = tool:WaitForChild("Throw") :: RemoteEvent local damage = 25 local reach = 40 local cooldown = 1 local lastThrow = -math.huge throw.OnServerEvent:Connect(function(_, target: unknown) if typeof(target) ~= "Instance" or not target:IsA("Player") then return end local holder = tool.Parent and Players:GetPlayerFromCharacter(tool.Parent) if not holder or holder == target then return end local now = os.clock() if now - lastThrow < cooldown then return end lastThrow = now local root = holder.Character and holder.Character:FindFirstChild("HumanoidRootPart") local targetRoot = target.Character and target.Character:FindFirstChild("HumanoidRootPart") local humanoid = target.Character and target.Character:FindFirstChildOfClass("Humanoid") if root and root:IsA("BasePart") and targetRoot and targetRoot:IsA("BasePart") and humanoid then if (root.Position - targetRoot.Position).Magnitude <= reach then humanoid:TakeDamage(damage) end end end)
Fixed example
Fixed
local tool = script.Parent local throw = tool:WaitForChild("Throw") :: RemoteEvent local damage = 25 local reach = 40 local cooldown = 1 local lastThrow = -math.huge throw.OnServerEvent:Connect(function(player: Player, target: unknown) local character = player.Character if not character or tool.Parent ~= character then return end if typeof(target) ~= "Instance" or not target:IsA("Player") or target == player then return end local now = os.clock() if now - lastThrow < cooldown then return end lastThrow = now local root = character:FindFirstChild("HumanoidRootPart") local targetRoot = target.Character and target.Character:FindFirstChild("HumanoidRootPart") local humanoid = target.Character and target.Character:FindFirstChildOfClass("Humanoid") if root and root:IsA("BasePart") and targetRoot and targetRoot:IsA("BasePart") and humanoid and humanoid.Health > 0 then if (root.Position - targetRoot.Position).Magnitude <= reach then humanoid:TakeDamage(damage) end end end)
Common false positives
- Remotes in
ReplicatedStoragethat use theplayerargument and look up that player's own tool, such asplayer.Character:FindFirstChild("Sword"), already act only for the caller. Tool.ActivatedandTool.Equippedconnections on the server are triggered by the holder's own client, so they don't need this check.- A remote that only ever changes the caller's own character, such as playing an emote on
player.Character, gives nothing away even if it sits in a tool.