Anti-Exploit for ClickDetectors and ProximityPrompts

Due to the replies made telling me to fix this code, after 2 years here I come! :smiley:


I am introducing a short module named “Protector” that will help you protect your ClickDetectors and ProximityPrompts from exploiters.

:red_circle: First of all, what are the issues with ClickDetectors and ProximityPrompts?

  • Roblox doesn’t seem to make distance checks for ClickDetectors, allowing exploiters to extend the MaxActivationDistance on the client and trigger them from anywhere on the map. (Or with an executor they can just trigger it)

  • Roblox doesn’t seem to check if the ProximityPrompt is enabled or not. If you disable a ProximityPrompt in your game expecting no players to interact with an object, exploiters will still be able to trigger that ProximityPrompt. (They seem to do some sort of distance check for ProximityPrompts this is why I didn’t added it in the module, though the range is kinda high)

:green_circle: How does this module work?

This module returns a table with a function named getSafeInput(Instance). You pass a ClickDetector/ProximityPrompt and it will return to you another table containing an event named Triggered which is the event you will connect to in order to receive safe input.

:large_blue_circle: Can you show me an example of how I would set up my code?

Have in mind that the Triggered event will trigger for both RightClick (false) / LeftClick (true) for ClickDetectors.

local protector = require(game.ServerStorage.Protector)
local Input1 = protector.getSafeInput(workspace.Part1.ClickDetector)
Input1.Triggered:Connect(function(Player, IsLeftClick)
	print(Player, IsLeftClick)
end)

local Input2 = protector.getSafeInput(workspace.Part2.ProximityPrompt)
Input2.Triggered:Connect(function(Player)
	print(Player)
end)

Module Link: Protector

Source Code:
--{- ⚜️ Main Objects ⚜️ -}--
local function IsPlayerAlive(Player) --> Determines if the player is alive depending on the Humanoid, HumanoidRootPart, and Health.
	local Character = Player.Character
	if Character and Character.Parent == workspace then
		local Humanoid = Character:FindFirstChildOfClass("Humanoid")
		local HumanoidRootPart = Character.PrimaryPart
		if Humanoid and Humanoid.Health > 0 and HumanoidRootPart then
			return true
		end
	end
	return false
end

local function GetBoundingBox(Object) --> Returns the CFrame and Size of Model/BasePart.
	if Object:IsA("BasePart") then
		return Object.CFrame, Object.Size
	elseif Object:IsA("Model") then
		return Object:GetBoundingBox()
	end
end

local function IsSafe(Player, InputObject) --> Determines if the ClickDetector/ProximityPrompt can be activated.
	if IsPlayerAlive(Player) then
		if InputObject:IsA("ClickDetector") then
			local ObjectCFrame, ObjectSize = GetBoundingBox(InputObject.Parent)
			local Position, Size = ObjectCFrame:PointToObjectSpace(Player.Character.PrimaryPart.Position), ObjectSize/2 + Vector3.one*InputObject.MaxActivationDistance
			return math.abs(Position.X) <= Size.X and math.abs(Position.Y) <= Size.Y and math.abs(Position.Z) <= Size.Z
		else --> Assumes is a ProximityPrompt
			return InputObject.Enabled --> We don't check for distance because Roblox already does it, though their check is kinda big.
		end
	end
	return false
end

--{- 🌐 Main Table 🌐 -}--
local protector = {}

------------------------------------------------------------------------------------------------------------------------------------------------------------------------||
---[]---[]---[]---[]---[]---[]---[]---[]---[]---[]---[]---[]---[]---[]---[]--- 🧾 Script Start 🧾 ---[]---[]---[]---[]---[]---[]---[]---[]---[]---[]---[]---[]---[]---[]---

---> 📚 ClickProtector Library 📚 <---
function protector.getSafeInput(InputObject) --> Returns a table with a Triggered event that you can connect to.
	local Triggered = Instance.new("BindableEvent")
	local Input = {}
	Input.Triggered = Triggered.Event
	
	local ObjectType = (typeof(InputObject) == "Instance" and InputObject.ClassName or typeof(InputObject))
	if ObjectType == "ClickDetector" then
		InputObject.MouseClick:Connect(function(Player)
			if IsSafe(Player, InputObject) then
				Triggered:Fire(Player, true) --> Left Click
			end
		end)

		InputObject.RightMouseClick:Connect(function(Player)
			if IsSafe(Player, InputObject) then
				Triggered:Fire(Player, false) --> Right Click
			end
		end)
	elseif ObjectType == "ProximityPrompt" then
		InputObject.Triggered:Connect(function(Player)
			if IsSafe(Player, InputObject) then
				Triggered:Fire(Player)
			end
		end)
	else
		error("invalid argument #1 to 'protector.getSafeInput' (ClickDetector or ProximityPrompt expected, got "..ObjectType..")", 2)
	end
	return Input
end

return protector --> By focasds :)

Thanks for using this module! (If you are not a fan of emojis in the code, you can remove them, I love to separate my code by comments and emojis)

26 Likes

An important thing to keep in mind if someone modifies this is that you should not punish players if they trigger the anti-exploit. A player with higher ping could click something and move out of the radius, and that could cause them to falsely trigger the anti-exploit. Instead of punishing them, just silently deny the action like the script currently does.

9 Likes

I’m surprised Roblox doesn’t have built in server side checks for this.

3 Likes

I’ll make this into a module tomorrow if I remember to.

Roblox rarely, if ever, checks for exploits. That’s totally on the developer. In this case, roblox checking for exploits like this may result in the click detector being changed from a local script to not function. (Not that you should be modifying things such as click detectors on the client)

1 Like

They could have it only do server side checks on the server side and not on localscripts.

I guess ping is a valid excuse though.
Plus, people might have something where their camera is seperate from their character’s body, though you could argue they could detect and account for that.

You do checks for if SetPositive(…) <= Size.X etc; you can just calculate the magnitude between the player and the clickdetector. This makes a much cleaner code.

1 Like

The code you have has a large number of issues.

For one, this would be best if it was modular and object-oriented. It’s not very efficient to get the part every time you click. For example, I would like to do SecureClick.New(ClickDetector) so that the following happen in order:

  • Constructor finds and stores information for MaxActivationDistance, RootPart, and whatever else you need.
  • Methods bind functions to the click detector

With that in mind, you should also handle exceptions; click detectors DO NOT have to be in a part, they may also remain in a model. Rather than assume an error, create a system to detect a primary part/choose one at random.

More exception handling should be apparent at places like this as well:

You are assuming that the Character exists, that the PrimaryPart exists, and that the Humanoid exists. Use logical constraints and FindFirstChild to properly handle edge cases for these.

Another issue I have with this is that you make a variable for math.abs. LuaU has inline caching, meaning that making variables for built-in libraries will actually be less performant than just typing the library function/value itself.

Lastly, the code is organized in a really weird manner. The below code really makes me scratch my head:

local SetPositive = math.abs
		local Position, Size = Part.CFrame:PointToObjectSpace(PrimaryPartPosition), Part.Size/2 + Vector3.new(MaxDistance, MaxDistance, MaxDistance)

		if SetPositive(Position.X) <= Size.X and SetPositive(Position.Y) <= Size.Y and SetPositive(Position.Z) <= Size.Z then
			if Humanoid.Health > 0 then Function(player) return end
			return
		end
		warn(player.Name.." is exploiting!")

Instead of making a complex system for detecting range, just do:

local Distance = (PrimaryPartPosition - ClickPartPosition).Magnitude -- "ClickPartPosition" would be a variable for the part's position.

if Distance <= MaxDistance then
    -- Removed "Humanoid.Health > 0" because that shouldn't be a default limitation
	Function(player)
else
    warn(player.Name .. " is out of range")
end

Good idea for this resource, but just make sure to take a look at some programming tips and guidelines on the internet to determine how efficient your code is.

2 Likes

make a module script with this

return function(ClickDetector, Function)
	if ClickDetector:IsA("ClickDetector") == false then warn("It should be an Instance of type: (ClickDetector)") return end

	ClickDetector.MouseClick:Connect(function(player)
		local Part = ClickDetector.Parent
		local Character = player.Character
		local PrimaryPartPosition = Character.PrimaryPart.Position
		local Humanoid = Character.Humanoid

		local MaxDistance = ClickDetector.MaxActivationDistance

		local SetPositive = math.abs
		local Position, Size = Part.CFrame:PointToObjectSpace(PrimaryPartPosition), Part.Size/2 + Vector3.new(MaxDistance, MaxDistance, MaxDistance)

		if SetPositive(Position.X) <= Size.X and SetPositive(Position.Y) <= Size.Y and SetPositive(Position.Z) <= Size.Z then
			if Humanoid.Health > 0 then Function(player) return end
			return
		end
		warn(player.Name.." is exploiting!")
	end)
end

so it can be used in any scripts with just this

local SecureClickDetector = require(<module>)

SecureClickDetector(script.Parent.ClickDetector, function(player)
	-- add your code here --
end)

There you go! I did a quick module for both ClickDetectors and ProximityPrompts.

1 Like