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:

text
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

lua
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.

lua
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.

lua
-- 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')
lua
-- 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.

lua
-- ESX
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer or xPlayer.job.name ~= 'police' then return end
lua
-- QBCore
local Player = QBCore.Functions.GetPlayer(source)
if not Player or Player.PlayerData.job.name ~= 'police' then return end

For admin actions, check an ACE permission instead of a client flag. See ACE permissions:

lua
if not IsPlayerAceAllowed(source, 'command.myadmin') then return end

Rewards 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.
lua
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 like giveMoney(playerId, amount) lets a cheater pay anyone.
  • Do not expose events you do not need. An event registered only with AddEventHandler, with no RegisterNetEvent, cannot be triggered from a client.
  • Do not run client strings as code. Never pass a client value to load, ExecuteCommand or 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 β†’

Keep reading