FiveM server events seguros: pare cheaters de abusarem net events
Cheaters disparam seus server events com qualquer valores. Como validar no server: use source, verifique distância, job e cooldowns, e mantenha preços em uma tabela de server.
Um script de shop em muitos servers parece assim, e é um botão de dinheiro grátis:
TriggerServerEvent('shop:buy', 'weapon_pistol', 1, -999999)Qualquer um com um cheat menu pode enviar aquela linha. O server roda o handler, tira um preço negativo do jogador e felizmente adiciona o dinheiro. Este guia mostra como escrever server events que não confiam no cliente.
A regra: o cliente não é confiável
Tudo que chega em um net event vem da máquina de um jogador, que aquele jogador controla. Trate cada argumento como entrada do usuário em um website: pode estar faltando, o tipo errado, negativo, grande ou simplesmente inventado.
O que o server consegue confiar:
source, o jogador que disparou o evento. O engine o define.- Dados que o server já tem: o job do jogador, inventory, dinheiro, posição, e suas próprias tabelas de config.
O que nunca deve confiar: quantias, preços, nomes de items, ids do jogador, coordenadas, nomes de jobs e flags de "sucesso" enviados pelo cliente.
Um exemplo ruim
RegisterNetEvent('shop:buy', function(item, count, price)
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.removeMoney(price * count)
xPlayer.addInventoryItem(item, count)
end)O cliente pega o item, a contagem e o preço. Um cheater compra qualquer coisa por nada, ou uma contagem negativa para imprimir dinheiro.
Um bom exemplo
Mantenha preços no server, valide os tipos, verifique onde o jogador é, depois age.
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)) no server precisa de OneSync habilitado. Se você ainda não fez, veja enabling OneSync.
O mesmo em QBCore e QBox
Apenas o lookup do jogador, o campo de job e a chamada de dinheiro mudam. A validação permanece a mesma.
-- 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')Verificações de job e permissão
Se um evento deve apenas funcionar para um job, o verifique no server dos dados do framework, não de um argumento.
-- 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 endPara ações de admin, verifique uma permissão ACE em vez de uma flag de cliente. Veja ACE permissions:
if not IsPlayerAceAllowed(source, 'command.myadmin') then return endRecompensas precisam de prova server-side
Eventos como job:finished ou mission:reward são o alvo clássico. Um cheater simplesmente os dispara em um loop.
- Mantenha o valor da recompensa em uma tabela de server, nunca nos argumentos do evento.
- Lembre no server que o jogador começou o job, e apenas pague se aquele estado existe. Limpe quando você pagar.
- Adicione um tempo mínimo entre começar e terminar.
- Verifique que o jogador está perto do ponto de entrega quando eles terminam.
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)Outros hábitos que ajudam
- Nunca use o id do jogador do cliente. Sempre use
source. Um evento comogiveMoney(playerId, amount)deixa um cheater pagar qualquer um. - Não exponha eventos que você não precisa. Um evento registrado apenas com
AddEventHandler, semRegisterNetEvent, não pode ser disparado de um cliente. - Não rode strings do cliente como código. Nunca passe um valor do cliente para
load,ExecuteCommandou uma string SQL. Use queries parametrizadas, como no oxmysql guide. - Registre os casos estranhos. Quando uma verificação falha, imprima o jogador e o nome do evento. Um padrão de falhas é como você encontra cheaters. Ban do txAdmin, veja moderating with txAdmin.
Atenção: esconder o nome do evento, obfuscar ou adicionar um "token" client-side não protege. O cliente consegue ler tudo aquilo. Apenas verificações no server contam.
Checklist
| Sintoma | Solução |
|---|---|
| Cliente envia o preço ou quantia | Mantenha uma tabela de preço no server e compute o total ali |
| Cliente envia um id do jogador | Use source em vez disso |
| Nome do item vem do cliente | Aceite apenas nomes encontrados em uma tabela de server |
| Quantias negativas ou grandes | Verifique type, math.floor, e um mínimo e máximo |
| Evento disparado do outro lado do mapa | Compare GetEntityCoords(GetPlayerPed(source)) com a posição de target |
| Evento spammado em um loop | Por-jogador cooldown, limpo em playerDropped |
| Ação só de job | Leia o job do framework no server |
| Evento de recompensa abusado | Armazene estado de job no server e pague uma quantia fixa uma vez |
Respostas rápidas
Um jogador pode realmente disparar meus server events?
Sim. Qualquer evento registrado com RegisterNetEvent pode ser disparado por um cliente modificado com qualquer argumento. O server não consegue dizer uma chamada de script real de uma forjada, então tem que validar tudo.
É seguro usar um id do jogador enviado pelo cliente?
Não. No server, source é definido pelo engine e não pode ser forjado. Um id do jogador, nome ou identificador enviado como um argumento pode, então nunca use para decidir quem recebe pagamento ou punição.
Preciso de um anticheat se eu valido meus eventos?
Validação é a base e para a maioria dos abuses de seus próprios scripts. Um anticheat adiciona detecção para coisas que eventos não conseguem ver, como menus injetados. Veja [FiveM anticheat basics](/blog/fivem-anticheat-basics).
Scripts sem esse problema
Shop CreatorMonte uma loja em menos de um minuto — donos, funcionários, cofres e assaltos inclusos.Ver script →
Item Creator V2Crie itens usáveis com animações, props, efeitos e muito mais — sem escrever código.Ver script →
Advanced BoostingBoosting de veículos pelo tablet: contratos da classe D à S+, crews e fila ao vivo.Ver script →