I saw another post recently related to preventing exploiters from tweaking your game in their favor. I noticed that in my weapon script, the cooldown variable is on the client, should I move it to the server instead and use a remote function to return for the cooldown to be true??
Yes, for most tools you should use a debounce on the server and the client side. The client side is to prevent them from sending too much data to the server.
The first rule in avoiding exploits is to “never trust the client.” Exploiters can trigger events with any kind of information that you allow. If you don’t want a variable to be controllable by the client, then it needs to run on the server instead.
Examples of server-controlled:
- Cooldown Time
- Damage
- Attack Speed
Examples of client-controlled data:
- Mouse Target
- Mouse Position
Just remember that the data received can be anything. Even different types of data, such as expecting a string value and receiving a number value instead. Create “sanity checks” in your server scripts to prevent these unexpected data events.
Examples of sanity checks:
- typeof(DataSentByPlayer) == “string”
- math.clamp(NumberSentByPlayer, 0, 100)
If you have the time, can you give me examples of how this can be used in code? And also, what would these do by themselves that prevent exploiting?
Say for example that we’re receiving information from the client to a RemoteEvent.
-- Assuming there is a folder in ServerStorage that holds "PlayerData" folders with data to be tracked.
-- Cash, Kills, Unlockables, etc.
PlayerDataFolder = game.ServerStorage.PlayerDataFolder
-- This script is tracking the total amount of cash that has been donated.
TotalCash = 1000
-- This script is tracking how many times a donation has been made. (Number of donation attempts.)
NumberOfDonations = 0
RemoteEvent.OnServerEvent:Connect(function( Player, DonationAmount )
-- Incrementing the number of donations.
NumberOfDonations += 1
-- Giving the donation to the total amount of cash donated.
TotalCash += DonationAmount
-- Taking the donated cash from the player's data.
local PlayerData = PlayerDataFolder:FindFirstChild( Player.UserId )
PlayerData.Cash.Value -= DonationAmount
end)
In the given example, there are a couple of things that can go wrong.
For the next example, assume the data being sent as “DonationAmount” is set to “Hello”:
RemoteEvent.OnServerEvent:Connect(function( Player, DonationAmount )
-- Donations will be incremented by 1.
-- This is unaffected by the value of DonationAmount.
NumberOfDonations += 1
-- Attempt to add a String value to a Number.
-- Errors, then stops running at this line.
TotalCash += DonationAmount
.
.
.
In the above example, the NumberOfDonations has been increased by 1. But, no transaction has occurred afterward. Because the script was expecting a Number value from the client and errored from performing arithmetic on a String.
Now, what happens if we send a negative number instead? Assuming “DonationAmount” has a value of -1000:
RemoteEvent.OnServerEvent:Connect(function( Player, DonationAmount )
-- Incrementing the number of donations.
NumberOfDonations += 1
-- Giving a negative number of -1000.
-- Taking 1000 instead of giving.
TotalCash += DonationAmount
-- Player receives 1000 cash instead of having it taken.
local PlayerData = PlayerDataFolder:FindFirstChild( Player.UserId )
PlayerData.Cash.Value -= DonationAmount
end)
In the above example, sending a negative number results in the exploiter receiving cash instead of spending it.
How can we prevent these two exploit scenarios? Well, we can introduce a few sanity checks by testing the data being sent by the client on the server script:
RemoteEvent.OnServerEvent:Connect(function( Player, DonationAmount )
-- Stop the function if the data received isn't the correct type.
if typeof( DonationAmount ) ~= "number" then
return
end
-- Make sure the amount of DonationAmount is within a reasonable range.
-- math.clamp() can be used to avoid negative inputs, or excessive number ranges.
local FixedDonationAmount = math.clamp( DonationAmount, 0, 1000 )
-- Take from the Player before performing certain actions, like low-impact transactions.
-- This deters exploiters from spamming RemoteEvents, because the consequence will always happen before the desired result.
local PlayerData = PlayerDataFolder:FindFirstChild( Player.UserId )
PlayerData.Cash.Value -= FixedDonationAmount
-- Using the adjusted FixedDonationAmount instead of the DonationAmount.
TotalCash += FixedDonationAmount
-- Increment donations after the donation has completed.
NumberOfDonations += 1
end)
But, the best case scenario to avoid needing to use sanity checks is to use as little information from the client as possible. You can do so by using fixed amounts:
PlayerDataFolder = game.ServerStorage.PlayerDataFolder
TotalCash = 1000
NumberOfDonations = 0
SetAmount = 100
Donate100RemoteEvent.OnServerEvent:Connect(function( Player )
local PlayerData = PlayerDataFolder:FindFirstChild( Player.UserId )
-- Simple sanity check. Make sure they have enough money.
-- Exploiters could still trigger this remote, even if they have less than the SetAmount.
if PlayerData.Cash.Value < SetAmount then
return
end
-- Take the amount we set.
PlayerData.Cash.Value -= SetAmount
-- Increase by set amount.
TotalCash += SetAmount
-- Increment counter.
NumberOfDonations += 1
end)
I hope this was informative.
This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.