Engineering
Writing server events that cannot be abused
Secure your FiveM net events: why any client can trigger any RegisterNetEvent, capturing source, validating types, ranges, distance, jobs and ownership, ACE checks, never trusting amounts or prices from the client, internal events with AddEventHandler, rate limits and logging.
Overview
Cheat menus ship with lists of event names from popular scripts, and one unprotected TriggerServerEvent('job:pay', 999999) is enough to wreck a server’s economy. The fix is not hiding event names; it is writing every server event as if the person calling it is hostile — because sometimes they are.
The classic exploit
RegisterNetEvent('fishing:sell', function(amount)
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.addMoney(amount * 50)
end)The client decides amount. A cheater calls TriggerServerEvent('fishing:sell', 100000) and gets five million.
The same event, secured
local SELL_POINT = vec3(-1845.0, -1195.0, 14.3)
local PRICE = 50
local last = {}
RegisterNetEvent('fishing:sell', function()
local src = source
local now = os.time()
if last[src] and now - last[src] < 5 then return end
last[src] = now
if #(GetEntityCoords(GetPlayerPed(src)) - SELL_POINT) > 5.0 then
print(('[fishing] %s tried to sell away from the market'):format(GetPlayerName(src)))
return
end
local count = exports.ox_inventory:GetItemCount(src, 'fish')
if count < 1 then return end
if exports.ox_inventory:RemoveItem(src, 'fish', count) then
exports.ox_inventory:AddItem(src, 'money', count * PRICE)
end
end)
AddEventHandler('playerDropped', function()
last[source] = nil
end)The client now sends nothing. The server counts the fish it knows about, removes them, and pays a price only it knows.
The validation checklist
| Check | How |
|---|---|
| Types | type(x) == 'number', math.type(x) == 'integer', string length limits |
| Ranges | Clamp or reject: if amount < 1 or amount > 10 then return end |
| Location | #(GetEntityCoords(GetPlayerPed(src)) - point) <= radius |
| Job / group | Read the job from your framework on the server |
| Permission | IsPlayerAceAllowed(src, 'myres.admin') |
| Ownership | Does this player own this vehicle / stash / house? |
| Items | Remove the item first; only reward if removal succeeded |
| Cooldown | Per-player timestamps, cleared on playerDropped |
| Entity IDs | DoesEntityExist(NetworkGetEntityFromNetworkId(netId)) and a relation to the player |
Why `local src = source` matters
source is a global that FiveM sets for the event currently running. If your handler waits (a database query with MySQL.query.await, a Wait), another event can run and change it. Copy it into a local on the first line and use only the local.
Internal events and callbacks
- Events that only server scripts trigger:
AddEventHandler('myres:internal', ...)withoutRegisterNetEvent— clients cannot call them. - Better still, use exports for server-to-server calls — see exports.
- For client requests that need an answer, use a callback (
lib.callback) and validate inside it exactly like an event. - Never send secrets, other players’ data or admin lists to clients with
TriggerClientEvent(-1, ...).
Log refusals
A validation failure is either a bug or an attack. Log it with the player’s name and identifiers; a burst of refusals from one player is your best cheat detector. For weapon-specific checks see weapon anticheat validation.
Frequently asked questions
Can players trigger my server events?
Yes. Any event registered with RegisterNetEvent can be triggered by any client with any arguments.
Does renaming events stop exploits?
No. Event names are visible in client code and network traffic. Validate instead.
Why should I copy source into a local variable?
source is global and can change after a yield such as a database query. A local keeps the right player.
How do I make an event only the server can call?
Register it with AddEventHandler only, without RegisterNetEvent, or use an export.
Should the client send the price of an item?
Never. Prices, amounts and rewards must come from server-side configuration.
Need this built, not just explained?
Ten years of FiveM work, from Lua to NUI
Custom resources, React NUI, ESX / QBCore / Qbox integration, OneSync performance audits and security reviews — plus the websites and SEO around your server brand.
Related guides
- EngineeringEvents in FiveM: how resources and players talkHandle events with AddEventHandler(name, fn); add RegisterNetEvent(name) (or use RegisterNetEvent(name, fn)) to allow the event to arrive over the network. TriggerEvent fires locally on the same side, TriggerServerEvent sends from a client to the server, and TriggerClientEvent(name, target, ...) sends from the server to one player (target = player ID) or everyone (-1). On the server, source is the sending player.
- EngineeringProtecting a FiveM server against cheatersBuild in layers: make the server the authority for money, items and entities; validate every net event; enable sv_entityLockdown once scripts spawn server-side; filter dangerous game events (explosionEvent, weaponDamageEvent, clearPedTasksEvent) on the server; ban on several identifiers plus GetPlayerToken tokens; add honeypot events; and log everything. A commercial anticheat adds client-side detection on top — it does not replace the rest.
- Getting startedClient-side vs server-side in FiveMClient scripts run on each player’s PC and handle what that player sees and does: drawing, input, markers, animations and local effects. Server scripts run once, on your server, and own everything that has value or must be trusted: money, items, jobs, the database and permissions. The client asks; the server decides. Never trust data sent by a client without checking it.
- FrameworksGetting started with ox_libStart ox_lib before your resource, add shared_script '@ox_lib/init.lua' and lua54 'yes' to your fxmanifest, and the global lib becomes available. Use cache.ped/cache.vehicle instead of calling natives in loops, lib.callback for client↔server requests, lib.notify, lib.progressBar, lib.registerContext/lib.showContext, lib.inputDialog, lib.points and lib.zones for interaction, and lib.addCommand for commands with ACE restrictions.