Secure FiveM server events: stop cheaters from abusing net events
Cheaters trigger your server events with any values. How to validate on the server: use source, check distance, job and cooldowns, and keep prices in a server table.
A shop script on a lot of servers looks like this, and it is a free money button:
TriggerServerEvent('shop:buy', 'weapon_pistol', 1, -999999)Anyone with a cheat menu can send that line. The server runs the handler, takes a negative price from the player and happily adds the money. This guide shows how to write server events that do not trust the client.
The rule: the client is not trusted
Everything that arrives in a net event comes from a player's machine, which that player controls. Treat each argument as user input on a website: it can be missing, the wrong type, negative, huge or simply made up.
What the server can trust:
source, the player who triggered the event. The engine sets it.- Data the server already holds: the player's job, inventory, money, position, and your own config tables.
What it must never trust: amounts, prices, item names, player ids, coordinates, job names and "success" flags sent by the client.
A bad example
RegisterNetEvent('shop:buy', function(item, count, price)
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.removeMoney(price * count)
xPlayer.addInventoryItem(item, count)
end)The client picks the item, the count and the price. A cheater buys anything for nothing, or a negative count to print money.
A good example
Keep prices on the server, validate the types, check where the player is, then act.
local Items = {
water = { price = 5, max = 10 },
bread = { price = 8, max = 10 },
}
local shopCoords = vec3(25.7, -1347.3, 29.5)
local lastBuy = {}
RegisterNetEvent('shop:buy', function(item, count)
local src = source
-- 1. types and ranges
if type(item) ~= 'string' or type(count) ~= 'number' then return end
count = math.floor(count)
local def = Items[item]
if not def or count < 1 or count > def.max then return end
-- 2. cooldown
local now = GetGameTimer()
if lastBuy[src] and now - lastBuy[src] < 1000 then return end
lastBuy[src] = now
-- 3. distance
local ped = GetPlayerPed(src)
if #(GetEntityCoords(ped) - shopCoords) > 5.0 then return end
-- 4. price comes from the server table
local total = def.price * count
local xPlayer = ESX.GetPlayerFromId(src)
if not xPlayer or xPlayer.getMoney() < total then return end
xPlayer.removeMoney(total)
xPlayer.addInventoryItem(item, count)
end)
AddEventHandler('playerDropped', function()
lastBuy[source] = nil
end)GetEntityCoords(GetPlayerPed(source)) on the server needs OneSync enabled. If you have not done that yet, see enabling OneSync.
The same on QBCore and QBox
Only the player lookup, the job field and the money call change. The validation stays the same.
-- QBCore
local QBCore = exports['qb-core']:GetCoreObject()
local Player = QBCore.Functions.GetPlayer(src)
if not Player or Player.PlayerData.money.cash < total then return end
Player.Functions.RemoveMoney('cash', total, 'shop-purchase')-- QBox
local Player = exports.qbx_core:GetPlayer(src)
if not Player or Player.PlayerData.money.cash < total then return end
Player.Functions.RemoveMoney('cash', total, 'shop-purchase')Job and permission checks
If an event should only work for a job, check it on the server from the framework's data, not from an argument.
-- ESX
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer or xPlayer.job.name ~= 'police' then return end-- QBCore
local Player = QBCore.Functions.GetPlayer(source)
if not Player or Player.PlayerData.job.name ~= 'police' then return endFor admin actions, check an ACE permission instead of a client flag. See ACE permissions:
if not IsPlayerAceAllowed(source, 'command.myadmin') then return endRewards need server-side proof
Events like job:finished or mission:reward are the classic target. A cheater simply fires them in a loop.
- Keep the reward amount in a server table, never in the event arguments.
- Remember on the server that the player started the job, and only pay if that state exists. Clear it when you pay.
- Add a minimum time between starting and finishing.
- Check the player is near the delivery point when they finish.
local activeJobs = {}
RegisterNetEvent('job:start', function()
activeJobs[source] = GetGameTimer()
end)
RegisterNetEvent('job:finish', function()
local src = source
local startedAt = activeJobs[src]
if not startedAt or GetGameTimer() - startedAt < 30000 then return end
activeJobs[src] = nil
-- pay a fixed amount defined on the server
end)Other habits that help
- Never use the client's player id. Always use
source. An event likegiveMoney(playerId, amount)lets a cheater pay anyone. - Do not expose events you do not need. An event registered only with
AddEventHandler, with noRegisterNetEvent, cannot be triggered from a client. - Do not run client strings as code. Never pass a client value to
load,ExecuteCommandor a SQL string. Use parameterised queries, as in the oxmysql guide. - Log the odd cases. When a check fails, print the player and the event name. A pattern of failures is how you find cheaters. Ban from txAdmin, see moderating with txAdmin.
Warning: hiding the event name, obfuscating it or adding a client-side "token" does not protect it. The client can read all of that. Only checks on the server count.
Checklist
| Symptom | Fix |
|---|---|
| Client sends the price or amount | Keep a price table on the server and compute the total there |
| Client sends a player id | Use source instead |
| Item name comes from the client | Accept only names found in a server table |
| Negative or huge amounts | Check type, math.floor, and a minimum and maximum |
| Event fired from across the map | Compare GetEntityCoords(GetPlayerPed(source)) with the target position |
| Event spammed in a loop | Per-player cooldown, cleared in playerDropped |
| Job-only action | Read the job from the framework on the server |
| Reward event abused | Store job state on the server and pay a fixed amount once |
Quick answers
Can a player really trigger my server events?
Yes. Any event registered with RegisterNetEvent can be triggered by a modified client with any arguments. The server cannot tell a real script call from a forged one, so it has to validate everything.
Is it safe to use a player id sent by the client?
No. On the server, source is set by the engine and cannot be forged. A player id, name or identifier sent as an argument can be, so never use it to decide who gets paid or punished.
Do I need an anticheat if I validate my events?
Validation is the base and stops most abuse of your own scripts. An anticheat adds detection for things events cannot see, such as injected menus. See [FiveM anticheat basics](/blog/fivem-anticheat-basics).
Scripts that skip this problem
Shop CreatorBuild a shop in under a minute β owners, employees, vaults and robberies included.View script β
Item Creator V2Create usable items with animations, props, effects and more β without writing code.View script β
Advanced BoostingTablet-driven vehicle boosting: contracts from class D to S+, crews and a live queue.View script β