Hi, I’ve been creating something lately and encountered this problem just now.
Let’s say that I was trying to make a button that adds money on click using a remote event:
I’d detect the click, fire the remote event to the server, and then add the money there, but I’ve noticed just now that this can be exploited and I’ve been doing this error since I started coding.
Is there a way to change/prevent this?
You can solve this with a simple cooldown. It’s important to always rely on the server as an authority (both for adding money and determining how much should be added etc.).
Additionally, a client-sided cooldown can be utilized to prevent spamming of remote events, which is going to work for the majority players except those who exploit. The client-sided debounce is intended solely to avoid sending unnecessary remote calls and additionally burdening the network with signals that are going to be ignored.
Client
local UIS = game:GetService("UserInputService")
local remoteEvent = game:GetService("ReplicatedStorage").RemoteEvent
local lastClick = os.clock()
local COOLDOWN = 1
UIS.InputBegan:Connect(function(input, gameProcessed)
if gameProcessed then return; end
if input.UserInputType == Enum.UserInputType.MouseButton1 then
-- current time - last click > cooldown time?
if os.clock() - lastClick > COOLDOWN then
print("Fired")
lastClick = os.clock() -- update last click time
remoteEvent:FireServer()
end
end
end)
Server
local remoteEvent = game:GetService("ReplicatedStorage").RemoteEvent
local lastClickByPlayer = {}
local COOLDOWN = 1
remoteEvent.OnServerEvent:Connect(function(player)
-- current time - last click > cooldown time?
if os.clock() - (lastClickByPlayer[player] or 0) > COOLDOWN then
lastClickByPlayer[player] = os.clock() -- update last click time
print("Accepted")
end
end)
-- clean up
game:GetService("Players").PlayerRemoving:Connect(function(player)
lastClickByPlayer[player] = nil
end)
I wouldn’t recommend the client do the rubber stamping of time, instead that logic should be done on the server, as you know what the people say “anything on the client would be spoofed and bypassed by the exploiters”.
In all my projects I also add the client sided debounce, which works very well. Sure, it can be avoided by an exploiter, which is why server-sided cooldown is in place.
I suppose there are small cases where a server might ignore the request if the latency was high at the time of first request and unusually low a moment later. We can combat that by having a slightly shorter cooldown on server compared to the client, although that depends on the use case.
@Turbino32 they’re actually right, if you are going to be firing the remote event very rapidly, the client-sided cooldown disadvantages overweight the benefits. Furthermore, the remote events have a 9 byte overhead, so, for example, 15 clicks per second wouldn’t even come close to 1% of available network bandwidth, unless a lot of data is sent along.
Yes, that can be exploited. Why not connect .Touched on server?
Also, .Touched can fire on every part of the character, so you may want to check whether the part is a component of the character, see whether player has recently activated .Touched, and then act accordingly.
Example with one part.
local Players = game:GetService("Players")
local part = script.Parent
local debouncePerPlayer = {}
part.Touched:Connect(function(hit)
local player = Players:GetPlayerFromCharacter(hit.Parent)
if player and not debouncePerPlayer[player] then
debouncePerPlayer[player] = true
print("Money added here.")
task.wait(1) -- 1 second debounce
debouncePerPlayer[player] = nil
end
end)
Thanks for the response! However using client-sided debounce is just not going to cut it for me or for some people who want full prevention of remote spamming. If we wanted to prevent such things we can do what @Quwanterz has said; which is to add a rate limiter.
Since I don’t know anything about rate limiters, here’s a link about it:
Additionally, on the lagging part you are correct, however if we shorten the cooldown of the player, wouldn’t that just enable them to get more i.e coins or points? Edit: it seems there’s like a function of Player:GetNetworkPing(), how would we implement such a thing? Since ping always varies like going up and down.
Thanks for opening this discussion. Interesting. Also, great article you sent.
I’ll have to do more research, though I’m not sure it’s possible to add a real rate-limiter in Roblox such as one of those used in web development.
Yes, please ignore the client-sided addition idea, it only works in cases where timing is not so crucial.
Hmm, I forgot this function even existed. Upon a closer look, the docs say it “returns the engine-calculated latency in seconds”.
We don’t exactly know how the returned ping is calculated, and last time I checked values differed a lot compared to the game console average, or ping received from a round trip via remote events.
Luckily, the previous docs mentioned a soft limit of about 50 kB/s per player. So it’s probably worth more to compress the data and to carefully build the communication. Besides, remote signals are replicated with 60 Hz frequency at best.
Thank you again, and I’ll carefully read that article before I (maybe) get back with any update.
Well, this discussion ended up being completely transformed into another topic lol, hopefully I’ll be more careful with this remote event thing during my journey, thanks!