5 min read
Securing FiveM server events: stop trigger exploits in your scripts
Why cheat menus can call any FiveM server event, and how to write RegisterNetEvent handlers and callbacks that validate source, input, distance, permissions and rate. Lua patterns for ESX and QBCore.
Parity Team
"Someone gave themselves 10 million dollars." "Someone spawned 500 of an item." "Someone jailed the whole server." On FiveM, these are almost never clever hacks. They are a cheat menu calling a server event that trusts whatever the client sends. Any client can trigger any event a resource registers with RegisterNetEvent, with any arguments, as often as it likes. Cheat menus ship lists of known event names for popular resources.
The fix is not to hide event names. Renaming esx_jobs:pay to something random only delays the next person who reads your client files. The fix is to write events that are safe to call with any input.
The five checks
Every server event and callback that changes something should answer five questions on the server, in this order.
1. Who is calling?
Use source. It is set by the server and cannot be faked. Never accept a player id from the client to mean "me".
RegisterNetEvent('garage:takeOut', function(plate)
local src = source -- the real caller
-- not: function(playerId, plate) ... using playerId
end)Copy source into a local at the top. After any Wait or callback, the global source may belong to a different event.
2. Is the input what you expect?
Check types, ranges and allowed values. Reject everything else silently.
local function int(v, min, max)
v = math.floor(tonumber(v) or -1)
if v < min or v > max then return nil end
return v
end
RegisterNetEvent('fishing:sell', function(fish, count)
local src = source
count = int(count, 1, 50)
if type(fish) ~= 'string' or not FISH_PRICES[fish] or not count then return end
-- ...
end)Prices, rewards and payouts come from server-side tables, never from arguments.
3. Is the player where they should be?
With OneSync the server knows every player's position. A player selling fish from the other side of the map, or robbing a store they are not standing in, is calling the event directly.
local SELL_POINT = vector3(-1845.0, -1196.0, 14.3)
local function isNear(src, coords, maxDistance)
local ped = GetPlayerPed(src)
if ped == 0 then return false end
return #(GetEntityCoords(ped) - coords) <= maxDistance
end
if not isNear(src, SELL_POINT, 10.0) then return end4. Is the player allowed to?
Job, grade, gang, ownership of the vehicle or item, whether a minigame actually started. Keep that state on the server.
-- QBCore
local Player = QBCore.Functions.GetPlayer(src)
if not Player or Player.PlayerData.job.name ~= 'police' then return end
-- ESX
local xPlayer = ESX.GetPlayerFromId(src)
if not xPlayer or xPlayer.job.name ~= 'mechanic' then return endFor multi-step actions (start a job, finish a job, get paid), store a token or a timestamp on the server when the step starts and check it when the client says it finished. A payout event that works without a started job is a money printer.
local activeRuns = {}
RegisterNetEvent('delivery:start', function()
local src = source
activeRuns[src] = { started = os.time() }
end)
RegisterNetEvent('delivery:complete', function()
local src = source
local run = activeRuns[src]
if not run or os.time() - run.started < 30 then return end -- too fast to be real
activeRuns[src] = nil
payDelivery(src)
end)
AddEventHandler('playerDropped', function()
activeRuns[source] = nil
end)5. How often?
Even a correct event becomes an exploit if it can be called 1,000 times a second. A per-player cooldown costs two lines.
local lastCall = {}
local function cooldown(src, key, ms)
local now = GetGameTimer()
lastCall[key] = lastCall[key] or {}
if lastCall[key][src] and now - lastCall[key][src] < ms then return false end
lastCall[key][src] = now
return true
end
RegisterNetEvent('crafting:craft', function(recipe)
local src = source
if not cooldown(src, 'craft', 2000) then return end
-- ...
end)Callbacks are events too
ESX.RegisterServerCallback, QBCore.Functions.CreateCallback and lib.callback.register are built on events. They need the same five checks. A callback that returns another player's inventory, phone messages or bank balance on request leaks data to anyone who asks.
Things that are not protection
- Obfuscating client scripts. The event names are still visible on the network and in memory.
- Checking on the client first. The cheat skips your client code and calls the server directly.
- A secret "key" argument sent from the client. Anything the client knows, the cheat knows.
- Kicking on unknown event names only. Useful as a tripwire, but the real events are the ones being abused.
Find the weak events you already have
- List every
RegisterNetEventand server callback in your server folder:grep -rn "RegisterNetEvent\|RegisterServerCallback\|CreateCallback\|callback.register" resources/. - Mark every one that gives money, items, weapons, vehicles or jobs, or affects another player.
- Check each against the five questions above. Start with money and items: that is where dupers go first.
- Log rejected calls with the player and the reason. A player who sends a sell event from across the map is not lagging.
Where an anticheat helps
Validated events stop players from abusing your scripts. They do not stop a modified client from aiming for them, flying, or injecting code, and you can't validate events in third-party resources you can't edit. An anticheat covers that ground and gives you evidence when it acts.
With Parity you can also turn your own checks into enforcement: the Lua SDK lets a resource capture evidence and log, kick or ban a player with your own reason, and no-code rules add your own actions (ban, kick, log, Discord or an HTTP call) to Parity's detections from the dashboard. Compare the options, or read the full FiveM server security guide.
