Cheats allow malicious clients to trigger events in any context. Learn how to secure client-server communication, implement server-side validation, and configure FXServer security convars.
The anti-cheat team is always trying to improve the anti-cheat, but sometimes things slip through.
In this guide we'll try to help cover some common-practice things you can do to make your server more secure by properly locking down your events.
Cheats can allow the client to trigger events in any context.
When we say context we mean they can execute client->server (via TriggerServerEvent) or client resource->client resource (via TriggerEvent).
When working with events in Lua, it's crucial to register them correctly based on whether they are called by the client or the server.
A common mistake is registering server events that are not supposed to be called by the client, or vice-versa, which can lead to security vulnerabilities.
Use AddEventHandler when the event is intended to be triggered within the same context, either client-client or server-server. This ensures that the events are not networked and cannot be called by the opposite side.
AddEventHandler("eventName", function(eventParam1, eventParam2)
-- Code here will be executed once the event is triggered in the same context.
end)Use RegisterNetEvent when the event needs to be triggered across different contexts, such as client-to-server or server-to-client.
RegisterNetEvent("eventName", function(eventParam1, eventParam2)
-- Code here will be executed once the event is triggered across different contexts.
end)This example is for the client, and like anything on the client, it is not foolproof and can be manipulated by cheating clients.
If you want to block execution from the same context (such as preventing a server-to-server network event from being triggered by the client), you must register your event and check the sender's source:
RegisterNetEvent("eventName", function(eventParam1, eventParam2)
-- Server will send network id `65535` for events originating from the server
if source ~= 65535 then return end
end)Even if you build a robust anti-cheat, adding checks on server events makes them significantly more secure. This is a highly recommended practice, although it won't prevent everything. Below we share some good tips.
This ensures the integrity and security of your gaming environment.
All examples below assume the use of a framework (such as ESX, QB-Core, etc.).
This is intended to show you bad ways to handle events, you should never do this. Directly adding items to the user from their own input is always bad practice: you must always validate user inputs.
RegisterNetEvent("job:givePlayerItem", function(item, count)
local ply = FX.GetPlayerFromSource(source)
-- Directly adding items to the user from their own input is dangerous!
ply.addItem(item, count)
end)Here is a robust security pattern. The server tracks the player's state and coordinates, and verifies actions using server-side ticks instead of relying solely on client triggers.
-- dummy coordinate
local VALID_JOB_COORD = vector3(125.0, 111.1, 35.83)
local MAX_ITEM_COUNT = 10
-- dummy coordinate
local VALID_TURNIN_COORD = vector3(1888.0, 1254.1, 48.0)
local ITEM_NAME = 'log'
-- list of players with an active job
local activeJobs = {}
AddEventHandler("playerDropped", function ()
if not activeJobs[source] then return end
activeJobs[source] = nil
end)
function isPedWithinRange(ped, tgtCoords)
return #(GetEntityCoords(ped) - tgtCoords) < 15.0
end
-- process job tick and increment items server-side
CreateThread(function()
while true do
for src, data in pairs(activeJobs) do
local ped = GetPlayerPed(src)
-- if they are not in range, we don't want to give them the item
if isPedWithinRange(ped, VALID_JOB_COORD) then
-- give them the item, but limit it to MAX_ITEM_COUNT
data.itemCount = math.min(data.itemCount + 1, MAX_ITEM_COUNT)
end
end
-- process job tick once per second
Wait(1000)
end
end)
RegisterNetEvent("job:startJob", function()
local ped = GetPlayerPed(source)
-- if they are within 15 units, they do the job
if isPedWithinRange(ped, VALID_JOB_COORD) then
activeJobs[source] = {
itemCount = 0,
}
end
end)
RegisterNetEvent("job:givePlayerItem", function()
local ply = FX.GetPlayerFromSource(source)
-- if they have no active job, they shouldn't reach this event!
local jobData = activeJobs[source]
if not jobData then return end
local ped = GetPlayerPed(source)
-- they are not within range of turn in coordinate, reject their changes
if not isPedWithinRange(ped, VALID_TURNIN_COORD) then return end
-- reset job data so they can't trigger it multiple times
activeJobs[source] = nil
-- add items to user validated server side
ply.addItem(ITEM_NAME, jobData.itemCount)
end)Please note that the following settings should not be changed unless you know exactly what you are doing. The Cfx.re / Adhesive team works very hard to prevent cheaters. Most of these features will be enabled by default with FXServer build version 8450 and above.
Turning these convars on will likely result in the player being kicked with the following reason: Connection to CNL timed out.
Being kicked does not mean the player is automatically globally banned (Global Banned). However, it provides a strong indication of player reliability, which is extremely useful in assessing their trust.
The provided codes are not meant to work by simple copy-paste. They are just some tips to prevent certain actions that could happen on the server. This requires some programming knowledge. You are always free to join our Discord to get additional help.