How to secure Touched rewards in Roblox

Coins, checkpoints and finish lines that pay on Touched without checking the character is there. How to add distance checks and debounces on the server.

criticalLuaudit check: touch-trusted-rewards

What it looks like

A server script pays out when a character touches a part: a coin, a checkpoint, a finish line, a kill brick that awards the killer, a button that gives items. The Touched handler trusts the touch and never checks that the character is actually there.

Luau
coin.Touched:Connect(function(hit)
	local player = Players:GetPlayerFromCharacter(hit.Parent)
	if player then
		reward(player)
	end
end)

How to detect it

  • Find every Touched and TouchEnded connection in server code, and every helper that wraps them, such as zone modules.
  • For each one that gives something to the player who touched it (currency, items, progress, a stage, a win), look for a server-side check that the touching part is really close to the part it touched, such as the distance between the character's HumanoidRootPart and the part.
  • For checkpoints and finish lines, also check order: the server should only accept the next stage, not any stage.
  • Look for a per-player debounce. Without one, a single touch can pay out many times as the character's parts touch one after another.

What an exploiter could do

A player's client simulates its own character, and touches involving the character are reported to the server from that client. An exploiter can make those touches happen without going near the part, so they can collect every coin on the map, skip straight to the last checkpoint or claim a finish they never reached.

How to fix it

  • In the handler, find the character's HumanoidRootPart and check its distance from the touched part against the part's size plus a small margin.
  • Add a per-player debounce, and for one-off pickups, record or remove the pickup before paying.
  • For stages, accept a checkpoint only if it's the player's current stage plus one.
  • The character's position also comes from the client, so pair this with server-side movement checks for anything valuable.

Vulnerable example

Vulnerable
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
local PlayerData = require(script.Parent.PlayerData)

local coins = Workspace:WaitForChild("Coins")
local value = 10
local respawn = 10

for _, coin in coins:GetChildren() do
	if coin:IsA("BasePart") then
		coin.Touched:Connect(function(hit)
			local player = Players:GetPlayerFromCharacter(hit.Parent)
			local profile = player and PlayerData.get(player)
			if not profile or coin.Transparency > 0 then
				return
			end
			coin.Transparency = 1
			profile.Coins += value
			task.delay(respawn, function()
				coin.Transparency = 0
			end)
		end)
	end
end

Fixed example

Fixed
local Players = game:GetService("Players")
local Workspace = game:GetService("Workspace")
local PlayerData = require(script.Parent.PlayerData)

local coins = Workspace:WaitForChild("Coins")
local value = 10
local respawn = 10
local margin = 4

for _, coin in coins:GetChildren() do
	if coin:IsA("BasePart") then
		coin.Touched:Connect(function(hit)
			local character = hit.Parent
			local player = Players:GetPlayerFromCharacter(character)
			local profile = player and PlayerData.get(player)
			local root = character and character:FindFirstChild("HumanoidRootPart")
			if not profile or not root or not root:IsA("BasePart") or coin.Transparency > 0 then
				return
			end
			if (root.Position - coin.Position).Magnitude > coin.Size.Magnitude / 2 + root.Size.Magnitude + margin then
				return
			end
			coin.Transparency = 1
			profile.Coins += value
			task.delay(respawn, function()
				coin.Transparency = 0
			end)
		end)
	end
end

Common false positives

  • Touches that only hurt the player who touched, such as a kill brick or lava, give an exploiter nothing. Avoiding them is their own business.
  • Touches between two parts the server owns, such as an anchored trap and a server-driven NPC, aren't reported by a client.
  • A zone module that checks positions on the server rather than using Touched already has the distance check built in. Read it before reporting.