How to secure ProximityPrompts and ClickDetectors on the server
Exploiters can fire prompt and click events from anywhere, ignoring Enabled and MaxActivationDistance. How to check distance, state and timing on the server.
What it looks like
A ProximityPrompt, ClickDetector or DragDetector hands out something, and the server handler trusts the object's own settings to limit who can use it and when: Enabled, MaxActivationDistance, RequiresLineOfSight or HoldDuration. Roblox's security guide says exploiters can fire these events from any distance, at any time, often ignoring those settings.
chest.ClickDetector.MouseClick:Connect(function(player) giveReward(player) end)
How to detect it
- Find every
ProximityPrompt,ClickDetectorandDragDetectorconnection in server code and what each one gives, changes or costs. ClickDetectorevents have no server-side checks at all.MaxActivationDistance, a value of 0 or a parent that can't be clicked doesn't stop them. Look for a distance check against the player's character and any state the action needs.- For
ProximityPrompt, onlyTriggeredhas a server-side distance check.PromptButtonHoldBegan,PromptButtonHoldEndedandTriggerEndedcan be sent by any client from anywhere.Enabledset to false on the server doesn't stop any of them, and the client controls how long it actually holds. - Look for actions that need a hold or a wait, such as lockpicking or channelling. The server must time them itself, from its own record of when the hold began, rather than trusting
HoldDuration. - For
DragDetector, onlyEnabledandPermissionPolicyare checked for drag events. Click events inherited fromClickDetectoraren't checked. - If the prompt or detector sits on an unanchored part, the player can take network ownership and bring the part to their character, so the distance check must not rely on that part's position alone. See Rewards decided by parts a client simulates.
- A disabled prompt or detector that the game switches on later, such as a chest that unlocks after a boss, needs a server-side record of whether it's open. Checking
Enabledon the server is enough only when the server is the only thing that changes it. - Rate-limit anything that gives currency or items, per player, on the server.
What an exploiter could do
They trigger every prompt and click detector in the map from wherever they stand: opening every chest, collecting every pickup, buying from every shop, starting locked events and finishing timed actions instantly. Disabled prompts for content that isn't open yet work too.
How to fix it
- In every handler, check the player's character exists, is alive and is close enough to the object, measured on the server from anchored parts.
- Keep the state that decides whether the object can be used, such as unlocked or cooling down, in a server table, and check it in the handler.
- Time holds and channels on the server from the moment the server saw them begin, and add a per-player cooldown.
Vulnerable example
local Workspace = game:GetService("Workspace") local PlayerData = require(script.Parent.PlayerData) local reward = 50 for _, chest in Workspace:WaitForChild("Chests"):GetChildren() do local detector = chest:FindFirstChildOfClass("ClickDetector") if detector then detector.MaxActivationDistance = 10 detector.MouseClick:Connect(function(player: Player) local profile = PlayerData.get(player) local key = "Chest:" .. chest.Name if profile and not profile.Claimed[key] then profile.Claimed[key] = true profile.Coins += reward end end) end end
Fixed example
local Workspace = game:GetService("Workspace") local PlayerData = require(script.Parent.PlayerData) local reward = 50 local reach = 12 for _, chest in Workspace:WaitForChild("Chests"):GetChildren() do local detector = chest:FindFirstChildOfClass("ClickDetector") if detector and chest:IsA("BasePart") and chest.Anchored then detector.MaxActivationDistance = 10 detector.MouseClick:Connect(function(player: Player) local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart") local humanoid = player.Character and player.Character:FindFirstChildOfClass("Humanoid") if not root or not root:IsA("BasePart") or not humanoid or humanoid.Health <= 0 then return end if (root.Position - chest.Position).Magnitude > reach then return end local profile = PlayerData.get(player) local key = "Chest:" .. chest.Name if profile and not profile.Claimed[key] then profile.Claimed[key] = true profile.Coins += reward end end) end end
Common false positives
- A prompt or detector whose action only affects the player who used it and gives them nothing, such as opening a door that closes again or playing a sound, needs no distance check.
ProximityPrompt.Triggeredhas a server-side distance check, so a prompt on an anchored part doesn't need its own. It still needs checks for any game state the action depends on. Roblox's guide warns that a prompt on an unanchored part can be pulled to the player's character, so that one needs a distance check too; see Rewards decided by parts a client simulates.- Handlers that check distance from the character to an anchored part on the server, like the fixed example, are the fix.