How to add server-side distance checks in Roblox

Chests, shops and sell pads that work from anywhere on the map. How to check on the server that the player's character is close enough to use them.

warningLuaudit check: missing-reach-check

What it looks like

The client asks to use something that sits in the world: open a chest, sell at a sell pad, buy from a stall, pick up an item, talk to an NPC. The server does it without checking that the player's character is anywhere near.

Luau
remote.OnServerEvent:Connect(function(player, chest)
	openChest(player, chest)
end)

How to detect it

  • Find handlers tied to a place in the world: they take a world Instance or the id of one, or they're only meant to work while the player stands somewhere, such as a button that only shows on a sell pad.
  • Look for a server-side distance check between the character's HumanoidRootPart and the object or area.
  • Checks done on the client, such as only firing the remote from a Touched event or only showing the button nearby, don't count. The client can fire the remote from anywhere.

What an exploiter could do

They use things from anywhere on the map: collect every chest without travelling, sell from spawn, or buy from a shop that's meant to be at the end of a level. Usually that's a time saving, but when reaching the place is the challenge it lets them skip progress.

How to fix it

  • On the server, check the distance from the character's HumanoidRootPart to the object, or to a part that marks the area, and allow a small margin for latency.
  • The character's position comes from the player's own client, so this stops use from a distance but not a client that also moves its character. Movement checks belong to anti-exploit.

Vulnerable example

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

local chests = Workspace:WaitForChild("Chests")
local reward = 50
local respawn = 600
local opened: { [Player]: { [Instance]: number } } = {}

ReplicatedStorage.Remotes.OpenChest.OnServerEvent:Connect(function(player: Player, chest: unknown)
	if typeof(chest) ~= "Instance" or not chest:IsA("BasePart") or chest.Parent ~= chests then
		return
	end
	local profile = PlayerData.get(player)
	if not profile then
		return
	end
	local times = opened[player] or {}
	opened[player] = times
	local now = os.clock()
	if times[chest] and now - times[chest] < respawn then
		return
	end
	times[chest] = now
	profile.Coins += reward
end)

Players.PlayerRemoving:Connect(function(player)
	opened[player] = nil
end)

Fixed example

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

local chests = Workspace:WaitForChild("Chests")
local reward = 50
local respawn = 600
local reach = 12
local opened: { [Player]: { [Instance]: number } } = {}

ReplicatedStorage.Remotes.OpenChest.OnServerEvent:Connect(function(player: Player, chest: unknown)
	if typeof(chest) ~= "Instance" or not chest:IsA("BasePart") or chest.Parent ~= chests then
		return
	end
	local character = player.Character
	local root = character and character:FindFirstChild("HumanoidRootPart")
	if not root or not root:IsA("BasePart") or (root.Position - chest.Position).Magnitude > reach then
		return
	end
	local profile = PlayerData.get(player)
	if not profile then
		return
	end
	local times = opened[player] or {}
	opened[player] = times
	local now = os.clock()
	if times[chest] and now - times[chest] < respawn then
		return
	end
	times[chest] = now
	profile.Coins += reward
end)

Players.PlayerRemoving:Connect(function(player)
	opened[player] = nil
end)

Common false positives

  • Menus meant to work anywhere, such as a shop button on the HUD, don't need a distance check.
  • Actions on the player's own things that give them nothing new, like rearranging furniture on their own plot, don't need one either.
  • A distance check inside a helper module counts. Read it before reporting.