How to check if a player is spamming a remote too fast?

Is that right? I was under the impression only variables in ModuleScripts required by the client could be changed if they please. How do you change a variable in a LocalScript while it’s running?

1 Like

Can you explain what this means?

Exploits have the debug library implemented which allows the use of functions such as debug.setupvalue, allowing them to change almost if not all variables they please in any localscript (or modulescript).

1 Like

I edited my reply to explain it a bit. But you’ll probably get a much better explanation here: Leaky bucket - Wikipedia

2 Likes

This wouldn’t effect the server though.

I can give you a simple, concrete example, as that wiki article is actually kind of verbose.

Suppose that your game lets a player swing their sword as fast as 3 times per second, and that attacking faster than this is only possible with an exploit. You might first think to have an attack counter for each player, that is checked and reset once per second on the server. The trouble with this is that it’s too simple and doesn’t account for variability in internet latency: I could legitimately attack a player at max attack speed for 2 seconds, a total of 6 attacks each 1/3 of a second apart (on my client). But, due to unequal transit times across the internet for my events, or a back up of event processing on the server, the server might see this as 2 attacks in the first second, and 4 attacks in the next 1-second interval. 4 attacks per second is above the max attack speed, but events from my client were valid!

So, what you do instead is something like this: Each attack you get from my client increase my attack counter. Once per second, you subtract up to 3 from my counter. Then after this subtraction, you compare the value of my attack counter against some threshold, which is a bit of an arbitrary safety margin. Suppose the threshold in this case is also set to 3 (which will tolerate a transient of up to 2x normal attack rate). What happens is my first 2 attacks make my counter 2. On the next check 1 second later, you try to subtract up to 3 from this, so my counter is now 0 (counter clamps at min of 0). 0 is less than the threshold of 3, so I’m still fine. On the next check, you process my 4 attacks, setting my counter to 4. The following second (during which I made no further attacks), you again start by subtracting 3, reducing my counter to 1. 1 is less than the threshold, so I’m still within the allowable tolerances.

Now, suppose I exploit and start sending 5 attacks per second. Well, you can see that adding 5 but subtracting 3 on each check will quickly overflow the bucket. My counter will go like this: 5-3=2(OK) 2+5-3=4(overflow!)

Basically, buffering the input in this manner makes it tolerant of events sometimes bunching up, yet still possible to validate the average rate of events that do naturally happen in bursts (like sword attacks). The tuning parameters for this kind of system are the bucket size (the threshold), and max allowed rate, and how often you check. Realistically, exploiters tend to just spam events at many times the normal rates, so you can be pretty generous with bucket size.

81 Likes

You can also upgrade the leaky buckets by instead of decrementing every so often, keep track of the last time the bucket was decremented. When a request comes in, decrement the bucket by the time since the last decrement divided by the time per a unit of leakage. This can be done with floating point values (like Lua numbers) or integers and there isn’t a need to perform rounding operations.

This should be very cheap too, nice suggestion Emily!

8 Likes

A simple way is to have a server sided dictionary, containing booleans stored by online user ids.

local debounces = {}

On every remote call, check if the boolean exists / is true. if debounces[userId] then…
if it does not exist or is false, set it to true, and set it to false after your function is done.

2 Likes

The drawback with this kind of system is that it has no tolerance for varying latency, it uses server arrival times, but the timings of events is not preserved through transmission over the internet. Valid event timings from a client can easily be rejected, which is frustrating for the player, especially if the client is doing any kind of prediction (like visually firing a shot, for which a projectile is then never created on the server).

In most cases, you want to make sure the average rate of client events is within sane limits, but in a way that tolerates variation in the arrival times of individual events. It’s not trusting the client completely, just giving the system enough margin of error to not start falsely rejecting valid input because of variations in ping.

2 Likes

Can’t you use os.time() difference checks instead then?

Yeah, and you can use tick() as well. This wouldn’t be as efficient though, because you’d need to implement a double sided queue. As requests came in and were accepted, you’d push a timestamp onto the top of the queue. To check if too many requests were coming in, you’d pop the oldest requests off the bottom of the queue until you reached one that was within a memory threshold (say, 10 seconds). You’d then compare the current number of requests in the queue to determine if there have been more than a desired amount in 10 seconds.

1 Like

I wouldn’t recommend it. If you trust client timestamps, you just give other types of exploiters (cheaters) something very obvious to tamper with.

For detecting true “spamming” of remotes, average rates at the server end are all you really care about, this isn’t the same use case as detecting players who are doing things like using skills before they go off cooldown, exploiters who are spamming remotes are generally not trying to cheat, they are trying to overload a remote that does something expensive (like cloning models from storage) in order to crash the server.

1 Like

You can use os.time() on the server :stuck_out_tongue: and just have a hashed table with last uses stored by userid.

I don’t see what use this would be on its own. What would you do with this data?

You could create a table of tick() for each player, and each time you fire set a variable like

local players = {}
newtick = tick()
if newtick > players[p]  + 1 then
print("All good")
end

This is just an example, so if it’s not enough time you can just change it up and make it better.

EDIT: Also players[p] in this situation would be the old tick() and after if it prints all good set players[p] to the new tick()

This is just another flavor of “time since last event” check. I’ve explained above why this isn’t a good general-purpose approach for flood control.

os.time() can be called on the server. So the argument of trusting the client is invalid in this case. That is why I don’t really understand why you don’t think it’s a simple way to prevent the spamming of remotes.

I know os.time() can be called on the server, I just don’t see how it’s useful. Let me illustrate. I’m playing your game, and I click 4 times to shoot a weapon, say each ~0.2 seconds apart. Each time I click, the game client fires a RemoteEvent, indicating that I fired a shot. Now, many miles away (possibly thousands), my RemoteEvents arrive at the game server, all at the same time and get processed (OnServerEvent) all on the same tick, with the same os.time stamp (they don’t need to be exact, but suppose the differences are way less than the original 0.2 seconds). Am I now a spammer with your system? I would not be with my system, because it is very forgiving of individual server arrival time differences.

1 Like

Yeah I guess you are right about that, but it’s a simple way to do it.
I’ll read your way of doing it because I haven’t had time to read it completely yet.

From what I understand and from personal experience, imagine if you have more than 1 player. If you have a denounce in that Event, and both players fire at the same time; It will only swing for one of the players and then wait(.5). As far as the solution that I’m trying to figure out, I simply have a Check for each time a player clicks every second. If clicked, Check = true. Waits(1) Check = false. Of course, this is server-side.