Hello Developers! As I’m sure all of you know, a large problem many games face is the abuse of their server side functions through remote events. To the inexperienced developer, exploiters have an insane amount of power, even after the rollout of filtering enabled. This guide/resource combo will help you easily go about implementing better practices and strong remote security. Before we begin though, allow me to introduce a useful tool that makes thorough type-based sanity checks extremely easy.
This is my simple Security Module, and it allows for advanced checks such as detecting math.huge or NaN to be done in a single line.
SecurityModuleBasic.rbxm (2.4 KB)
We’re going to split this guide into three parts: Part One discusses basic type checking, Part Two discusses what additional types of sanity checks you may need and simple ways to implement common non-type checks, and Part Three discusses other things you can do to secure your remotes.
Part One: Basic Type Checking
Using tools such as Remote Spy, exploiters are able to see what they’re sending to each remote, and can try to send incorrect data types to try and break your game. An example of this is disguising a table as an object or sending over a NaN value instead of a number. We can very easily combat this through type checking, but it can be annoying to do thorough checks, especially for niche things. Using the Security Module, you can make this process very easy, here’s an example:
--In this example, exampleDictionary looks like this:
{
Color = Color3.new(1,0,0),
Type = "Bullet",
}
RE.OnServerEvent:Connect(function(plr,velocity,root,exampleDictionary)
--First, we need to validate every type to make sure we're getting the correct information, as the exploiter could send anything in its place.
if Security.typecheckVector3(velocity,plr) == false then plr:Kick("What?") return end
--If they sent either the wrong data type or nil, they will be kicked from the game, as they cannot normally send either.
--The security module doesn't have an instance check, so we just have to do this one manually.
if typeof(root) ~= "Instance" then plr:Kick("What?") return end
--Back to the security module
if security.typecheckTable(exampleDictionary,plr) == false then plr:Kick("What?") return end
--Seeing as we know the content of the table, and we know that "Color" and "Type" should be a Color3 and a string respectively, we will also check for those.
if security.typecheckColor(exampleDictionary.Color,plr) == false then plr:Kick("What?") return end
if security.typecheckString(exampleDictionary.Type,plr) == false then plr:Kick("What?") return end
--With just 5 lines, we have made sure that exploiters have to use the data types we would normally use.
end)
Punishment Severity: Kick or (maybe) Ban. Since normal players can’t change the types of what they send, you know that only exploiters will be caught. Kicking is MUCH better since you won’t permanently ban an innocent player in the event of a false positive. If your code isn’t very sound and can’t account for everything (one of the data types may be nil on the client), then there’s a chance it can ruin an innocent player’s experience and prevent them from playing your game. If you want to ban exploiters for this, make sure you’re very smart about your client side code!!!
Part Two: Additional Sanity Checks
Although type checks are great, exploiters can still work within your limits to abuse your remotes. I’m going to cover some common exploits as well as the respective sanity checks you may need to combat them.
- Distance Check
Let’s say that you have a remote event that damages an npc or another player. Exploiters can abuse this to damage players from very far away when they normally shouldn’t. We can fix this with a simple distance check. Here’s an example:
InflictRemote.OnServerEvent:Connect(function(plr,otherCharacter,damage)
--First, we're going to do type checking
if typeof(otherCharacter) ~= "Instance" then plr:Kick("What?") return end
if not otherCharacter:IsA("Model") or not otherCharacter:FindFirstChildOfClass("Humanoid") or not otherCharacter:IsDescendantOf(workspace) then return end
--We won't kick the player because sometimes they can send the remote right before the player/npc despawns, we're just going to chose to do nothing instead.
if Security.typecheckNumber(damage,plr) == false then plr:Kick("What?") return end
--Let's also make sure the player isn't dealing an absurd amount of damage for some reason
if damage <= 0 or damage > 35 then return end
--Time to calculate the distance between the two players.
local P1,P2 = plr.Character:WaitForChild("HumanoidRootPart").Position,otherCharacter:WaitForChild("HumanoidRootPart").Position
--You may also want to make sure that both models actually have a HumanoidRootPart, just to be safe
local distance = (P1-P2).Magnitude
if distance >= 25 then return end
--The two characters were too far to reasonably damage each other.
end)
Punishment Severity: None. Laggy players may be able to deal damage from further distances away, so we’re just going to prevent it from doing anything rather than punishing the player
- Incorrect Choice From A List Of Strings
In this example, you’re allowing the player to choose between multiple classes, but there is an additional class that the player needs to unlock and can’t be accessed yet in this interaction. The exploiter is able to set their class as the locked class through this interaction. We’re going to use table.find() to fix this problem. Here’s an example:
--There are six ability classes, and three of them are starters while the other three are locked.
--The starters are Void, Solar, and Arc, while the locked ones are Stasis, Strand, and Prismatic
local StarterClasses = {"Void","Solar","Arc"}
ClassSelectRemote.OnServerEvent:Connect(function(plr,class)
if Security.typecheckString(class,plr) == false then plr:Kick("What?") return end
--We're going to make sure they can only choose from a select amount of options.
if not table.find(StarterClasses,class) then banFunction(plr,"Hey, that's locked!") return end
--Set their class
end)
Punishment Severity: Permanent Ban. Only exploiters can do this, ban them.
- Number Is Not An Integer And Is Too High Or Low.
Sometimes you may need to send a specific range of numbers or integers. Personally I like to use integers instead of strings to determine the type of a remote, such as 0 and 1 instead of “Normal” and “Critical”. In this situation, an exploiter can send any number instead of the ones we want or need. We can fix this by either table.find() (if you’re using integers as replacements for type variables) or through a math.floor() check and greater than/less than. Here’s an example:
--Variation 1 means Startup, Variation 2 means Normal, Variation 3 means Special
--We're also going to assume number is an integer between 1 and 10
local ValidVariants = {1,2,3}
IntegerRemote.OnServerEvent:Connect(function(plr,number,variation)
if Security.typecheckNumber(number,plr) == false then plr:Kick("What?") return end
if Security.typecheckNumber(variation,plr) == false then plr:Kick("What?") return end
--table.find() check for variant.
if not table.find(ValidVariants,variation) then banFunction(plr,"Seriously?") return end
--Let's ensure that the number is an integer and within the valid range.
if math.floor(number) ~= number or number < 1 or number > 10 then banFunction(plr,"Seriously?") return end
end)
Punishment Severity: Permanent Ban. Only exploiters can do this, ban them.
Part Three: Other Ways To Secure Remotes (INTERMEDIATE)
This part is probably the most difficult to implement into your game. I just have three things to talk about here. First, use a networking module (I recommend Warp!) Networking modules are great, they make events fire faster and more efficiently, can encrypt remote names, and expand upon the base tools ROBLOX provides (such as timeouts for remote functions and built in rate limiting). Second, make sure players are only firing remotes they’re supposed to. For example, if you have a remote for an admin gui, make sure only admins can fire that remote with a whitelist on the server. Another example is an effects replication remote that the server uses to fire to all clients. You don’t need to fire that to the server, so ban any player that does fire one. Third, Rate limit crucial remotes. This is pretty obvious, but players shouldn’t be able to deal damage 300 times a second, or fire a setup/initialized/loaded remote more than once. You shouldn’t ban for reaching the rate limit because of lag spikes, but you should definitely kick the player who reached it. Here’s one way that you could implement rate limiting:
local RATE = 50
local PlayerMap = {}
Players.PlayerAdded:Connect(function(plr)
PlayerMap[plr] = {tick(),0}
end)
Players.PlayerRemoving:Connect(function(plr)
PlayerMap[plr] = nil
end)
for _,plr in ipairs(Players:GetPlayers()) do
PlayerMap[plr] = {tick(),0}
end
Remote.OnServerEvent:Connect(function(plr)
--Rate Limiting
if tick()-PlayerMap[plr][1] < 1 then
PlayerMap[plr][2] += 1
if PlayerMap[plr][2] > RATE then plr:Kick("Rate Limit Reached") return end
else
PlayerMap[plr][1] = tick()
PlayerMap[plr][2] = 1
end
--Code
end)
Hopefully this guide was extremely helpful, and hopefully you can make very good use of this information. By taking the extra time to add in 3-16 lines, you can protect your playerbase from being bullied by exploiters. Good luck and have a good day!