Notice!
This guide is now quite out-of-date and in need of a rewrite. While much of the sentiment and the generalised statements made in the post remain true, most of the methods discussed are no longer relevant. I don’t have the time as of now to properly update this thread but I have removed some of the sections which have lead to some frankly incorrect assumptions on how much control is available over the client-side of the FiveM scripting environment. The original content of the post will now follow:
Hello again!
Following on from my last post about new frameworks, I thought it may also be a good idea to share my knowledge with the community on how hackers exploit servers and how to best protect your server from them.
First though, it needs to be pointed out that hackers, like any other person with malicious intent, are always going to find new exploits and ways around defences that have been put in place to stop them. FiveM’s developers work tirelessly to not only ban everyone who cheats but keep innocent players from getting caught in the crossfire. However, there are always people who manage to slip through the cracks. This is where you come in! Your server’s security should be of utmost importance. Even when you live in a gated community, you still lock the front door at night so why would you leave your server wide open for people to exploit?
In order to understand how you can patch exploits on your server, you need to know what hackers are even exploiting in the first place.
What currently hackers exploit
Almost every hack available for FiveM at the time of writing focus around running arbitrary Lua code on the client. In an ideal world, this wouldn’t matter however, we do not live in an ideal world. Being able to run any client-side Lua code allows access to game functions or natives such as CreateVehicle
which spawns vehicles or AddExplosion
which creates explosions.
However, what gets abused is the most is the TriggerServerEvent
function. TriggerServerEvent
is used to send signals to the server, asking it to run Lua code defined in the server script files. On paper, seems completely fine and necessary for communication between the client and server but sadly, the majority of resources do not take into account clients being able to trigger events arbitrarily. Take for example the esx_pizza
resource.
In that resource, there is a server event named esx_pizza:pay
. Going by the name, when a client triggers that event, the server figures out how much money the player who triggered it should get depending on what work they did, and then give it to them, right? Wrong.
Here is the entire server-side code for that resource:
ESX = nil
TriggerEvent('esx:getSharedObject', function(obj) ESX = obj end)
RegisterServerEvent('esx_pizza:pay')
AddEventHandler('esx_pizza:pay', function(amount)
local _source = source
local xPlayer = ESX.GetPlayerFromId(_source)
xPlayer.addMoney(tonumber(amount))
end)
I understand not all of you can read Lua code so I’ll try to give a simple explanation. When the job has been completed, the client triggers the esx_pizza:pay
event (using TriggerServerEvent
) along with an amount
value. amount
is equal to the amount of money the client should get. When the server receives the event, the server runs the code within AddEventHandler
. That code gets the amount
value sent to it by the client and just adds amount
to the client’s total money.
See where the issue is? There are no checks for if the player completed the job recently, no validation of the amount given. The client controls how much the server gives to them, not the other way around. This means a hacker can just execute the code that triggers the esx_pizza:pay
event with some obscene amount
value and give themselves millions of dollars, whenever they feel like it!
So, what can we do?
Patching resources
Well bad news is there is no easy one-size-fits all fix to resource vulnerabilities. You (or ideally the individual resource makers) will need go find exploitable events in the server-side code and rewrite that code to remove the exploit.
As an example of what to look for, let’s go back to esx_pizza
. The first thing to do is look at the client-side code and find exactly where esx_pizza:pay
is called. Using my code editor’s find function, it shows me that esx_pizza:pay
gets called in the following function:
Payout Function (original)
function donnerlapaye()
ped = GetPlayerPed(-1)
vehicle = GetVehiclePedIsIn(ped, false)
vievehicule = GetVehicleEngineHealth(vehicle)
calculargentretire = round(viemaxvehicule-vievehicule)
if calculargentretire <= 0 then
argentretire = 0
else
argentretire = calculargentretire
end
ESX.Game.DeleteVehicle(vehicle)
local amount = livraisonTotalPaye-argentretire
if vievehicule >= 1 then
if livraisonTotalPaye == 0 then
ESX.ShowNotification(_U('not_delivery'))
ESX.ShowNotification(_U('pay_repair'))
ESX.ShowNotification(_U('repair_minus')..argentretire)
TriggerServerEvent("esx_pizza:pay", amount)
livraisonTotalPaye = 0
else
if argentretire <= 0 then
ESX.ShowNotification(_U('shipments_plus')..livraisonTotalPaye)
TriggerServerEvent("esx_pizza:pay", amount)
livraisonTotalPaye = 0
else
ESX.ShowNotification(_U('shipments_plus')..livraisonTotalPaye)
ESX.ShowNotification(_U('repair_minus')..argentretire)
TriggerServerEvent("esx_pizza:pay", amount)
livraisonTotalPaye = 0
end
end
else
if livraisonTotalPaye ~= 0 and amount <= 0 then
ESX.ShowNotification(_U('truck_state'))
livraisonTotalPaye = 0
else
if argentretire <= 0 then
ESX.ShowNotification(_U('shipments_plus')..livraisonTotalPaye)
TriggerServerEvent("esx_pizza:pay", amount)
livraisonTotalPaye = 0
else
ESX.ShowNotification(_U('shipments_plus')..livraisonTotalPaye)
ESX.ShowNotification(_U('repair_minus')..argentretire)
TriggerServerEvent("esx_pizza:pay", amount)
livraisonTotalPaye = 0
end
end
end
end
This code is a bit more complex and is also in french so I translated it and added some comments so it’s easier to see what’s going on:
Payout Function
function donnerlapaye()
local ped = PlayerPedId() -- This is the player's ped
local jobVehicle = GetVehiclePedIsIn(ped, false) -- This is the player's vehicle that they are currently sitting in
local jobVehicleHealth = GetVehicleEngineHealth(jobVehicle) -- This is that vehicles health
local jobVehicleDamage = round(jobVehicleMaxHealth-jobVehicleHealth) -- This is the job vehicles damage
if jobVehicleDamage <= 0 then
jobVehicleDamage = 0
end
ESX.Game.DeleteVehicle(jobVehicle)
local amount = jobCurrentPayout-jobVehicleDamage
if jobVehicle >= 1 then
if jobCurrentPayout == 0 then
ESX.ShowNotification(_U('not_delivery'))
ESX.ShowNotification(_U('pay_repair'))
ESX.ShowNotification(_U('repair_minus')..jobVehicleDamage)
TriggerServerEvent("esx_pizza:pay", amount)
jobCurrentPayout = 0
else
if jobVehicleDamage <= 0 then
ESX.ShowNotification(_U('shipments_plus')..jobCurrentPayout)
TriggerServerEvent("esx_pizza:pay", amount)
jobCurrentPayout = 0
else
ESX.ShowNotification(_U('shipments_plus')..jobCurrentPayout)
ESX.ShowNotification(_U('repair_minus')..jobVehicleDamage)
TriggerServerEvent("esx_pizza:pay", amount)
jobCurrentPayout = 0
end
end
else
if jobCurrentPayout ~= 0 and amount <= 0 then
ESX.ShowNotification(_U('truck_state'))
jobCurrentPayout = 0
else
if jobVehicleDamage <= 0 then
ESX.ShowNotification(_U('shipments_plus')..jobCurrentPayout)
TriggerServerEvent("esx_pizza:pay", amount)
jobCurrentPayout = 0
else
ESX.ShowNotification(_U('shipments_plus')..jobCurrentPayout)
ESX.ShowNotification(_U('repair_minus')..jobVehicleDamage)
TriggerServerEvent("esx_pizza:pay", amount)
jobCurrentPayout = 0
end
end
end
end
So, to put that code simply, it first gets the job vehicle’s health, calculates its damage, takes that from the potential payout and then tells the server to give that money to the player. The massive problem is that the entire script is client sided and if the client has to give the server any of the payment values, it is exploitable! As I showed before, the server code is only that vulnerable function and nothing else. This means this script would have to be rewritten so that the values are not calculated on the client but rather on the server. While this adds additional complexity, it also makes it 1000x harder to exploit.
Thanks to @TheIndra, I now have an example to give of how to do it right! secure-resource-examples/esx_potjob at master · TheIndra55/secure-resource-examples · GitHub is a great example however, this one resource does not cover everything (and how could it!) so when rewriting scripts and especially writing new ones, you should always remember, never trust the client.
Validating client actions
So, now I’ll assume you’ve patched out every exploitable event in your entire server. That’s great! Now you can start being on the offensive and make sure clients that join your server aren’t allowed to do things that you haven’t allowed to happen. What I mean by that is checking that client’s aren’t doing things that are impossible.
Event Honeypots
The most effective way to do this is through an event honeypot. The concept is to setup a bunch of event handlers for events that are commonly exploited by hackers (and their hack menus) but don’t actually exist on your own server. Somebody ran es_admin:banPlayer
and you don’t even run an ESX server? Ban. Somebody else tried to jail a player without being a police officer? Stop the player from being jailed. The possibilities are endless!
Now you could set up an event handler that responds all events and ban when an event is run that isn’t handled by any of your scripts. However, this is currently a bad idea as FiveM updates add new net events all the time. One update is all it takes to automatically ban anyone who connects to your server until you add that event to your whitelist. The good news is though, this will potentially be a viable option with event documentation in the works and providing that all events are documented going forward, one could set up a system which scrapes the event documentation for new events and automatically whitelists them.
I hope this helps everyone!