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.

criticalLuaudit check: unchecked-tool-owner

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, or tool:IsDescendantOf(player) for an unequipped tool where using it from the backpack is allowed.
  • A handler that ignores the player argument and works out the attacker from tool.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, never tool.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 ReplicatedStorage that use the player argument and look up that player's own tool, such as player.Character:FindFirstChild("Sword"), already act only for the caller.
  • Tool.Activated and Tool.Equipped connections 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.