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:

text
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

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

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

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

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.

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

Para ações de admin, verifique uma permissão ACE em vez de uma flag de cliente. Veja ACE permissions:

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

Recompensas 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.
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)

Outros hábitos que ajudam

  • Nunca use o id do jogador do cliente. Sempre use source. Um evento como giveMoney(playerId, amount) deixa um cheater pagar qualquer um.
  • Não exponha eventos que você não precisa. Um evento registrado apenas com AddEventHandler, sem RegisterNetEvent, 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, ExecuteCommand ou 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 →

Continue lendo