Proteggere gli eventi del server FiveM: ferma i cheater dall'abusare degli eventi di rete

I cheater attivano i tuoi eventi server con qualsiasi valore. Come convalidare sul server: usa source, controlla la distanza, il job e i cooldown, e mantieni i prezzi in una tabella del server.

Uno script di negozio su molti server sembra questo, ed è un bottone di soldi gratuiti:

text
TriggerServerEvent('shop:buy', 'weapon_pistol', 1, -999999)

Chiunque abbia un menu di cheat può inviare quella riga. Il server esegue l'handler, prende un prezzo negativo dal giocatore e felicemente aggiunge i soldi. Questa guida mostra come scrivere eventi server che non fidano del client.

La regola: il client non è fidato

Tutto ciò che arriva in un evento di rete viene dalla macchina di un giocatore, che quel giocatore controlla. Tratta ogni argomento come input utente su un sito web: può essere mancante, il tipo sbagliato, negativo, enorme o semplicemente inventato.

Cosa il server può fidare:

  • source, il giocatore che ha attivato l'evento. Il motore lo imposta.
  • I dati che il server già tiene: il job del giocatore, l'inventario, i soldi, la posizione, e le tue stesse tabelle di config.

Cosa non deve mai fidare: quantità, prezzi, nomi di articoli, id di giocatori, coordinate, nomi di job e flag « success » inviati dal client.

Un cattivo esempio

lua
RegisterNetEvent('shop:buy', function(item, count, price)
    local xPlayer = ESX.GetPlayerFromId(source)
    xPlayer.removeMoney(price * count)
    xPlayer.addInventoryItem(item, count)
end)

Il client sceglie l'articolo, la quantità e il prezzo. Un cheater compra qualsiasi cosa per nulla, o una quantità negativa per stampare soldi.

Un buon esempio

Mantieni i prezzi sul server, convalida i tipi, controlla dove è il giocatore, poi agisci.

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)) sul server ha bisogno di OneSync abilitato. Se non l'hai ancora fatto, vedi abilitare OneSync.

Lo stesso su QBCore e QBox

Solo la ricerca del giocatore, il campo job e la chiamata di soldi cambiano. La convalida rimane la stessa.

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

Controlli di job e permesso

Se un evento dovrebbe funzionare solo per un job, controllalo sul server dai dati del framework, non da un argomento.

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

Per le azioni di admin, controlla un permesso ACE invece di un flag client. Vedi permessi ACE:

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

Le ricompense hanno bisogno di prova lato server

Eventi come job:finished o mission:reward sono il classico bersaglio. Un cheater semplicemente li attiva in un loop.

  • Mantieni l'importo della ricompensa in una tabella del server, mai negli argomenti dell'evento.
  • Ricorda sul server che il giocatore ha iniziato il job, e pagalo solo se quello stato esiste. Cancellalo quando paghi.
  • Aggiungi un tempo minimo tra l'inizio e la fine.
  • Controlla che il giocatore sia vicino al punto di consegna quando finisce.
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)

Altre abitudini che aiutano

  • Non usare mai l'id del giocatore dal client. Usa sempre source. Un evento come giveMoney(playerId, amount) permette a un cheater di pagare chiunque.
  • Non esporre eventi di cui non hai bisogno. Un evento registrato solo con AddEventHandler, senza RegisterNetEvent, non può essere attivato da un client.
  • Non eseguire stringhe client come codice. Non passare mai un valore client a load, ExecuteCommand o una stringa SQL. Usa query parametrizzate, come nella guida oxmysql.
  • Registra i casi strani. Quando un controllo fallisce, stampa il giocatore e il nome dell'evento. Un modello di fallimenti è come trovi i cheater. Banna da txAdmin, vedi moderating with txAdmin.

Attenzione: nascondere il nome dell'evento, offuscarlo o aggiungere un « token » lato client non lo protegge. Il client può leggere tutto quello. Solo i controlli sul server contano.

Checklist

Sintomo Soluzione
Il client invia il prezzo o la quantità Mantieni una tabella di prezzi sul server e calcola il totale lì
Il client invia un id di giocatore Usa source invece
Il nome dell'articolo viene dal client Accetta solo nomi trovati in una tabella del server
Quantità negative o enormi Controlla type, math.floor, e un minimo e un massimo
Evento attivato da tutta la mappa Confronta GetEntityCoords(GetPlayerPed(source)) con la posizione del bersaglio
Evento spammato in un loop Per-player cooldown, cancellato in playerDropped
Azione solo job Leggi il job dal framework sul server
Evento di ricompensa abusato Memorizz lo stato del job sul server e paghi un importo fisso una volta

Risposte rapide

Un giocatore può veramente attivare i miei eventi server?

Sì. Qualsiasi evento registrato con RegisterNetEvent può essere attivato da un client modificato con qualsiasi argomento. Il server non può dire una vera chiamata di script da una falsa, quindi deve convalidare tutto.

È sicuro usare un id di giocatore inviato dal client?

No. Sul server, source è impostato dal motore e non può essere falsificato. Un id di giocatore, nome o identificatore inviato come argomento può esserlo, quindi non lo usare mai per decidere chi viene pagato o punito.

Ho bisogno di un anticheat se convalido i miei eventi?

La convalida è la base e ferma la maggior parte dell'abuso dei tuoi script. Un anticheat aggiunge rilevamento per cose che gli eventi non possono vedere, come menu iniettati. Vedi [FiveM anticheat basics](/blog/fivem-anticheat-basics).

Script senza questo problema

Shop CreatorCrea un negozio in meno di un minuto — proprietari, dipendenti, casseforti e rapine inclusi.Vedi script →Item Creator V2Crea oggetti utilizzabili con animazioni, props, effetti e altro — senza scrivere codice.Vedi script →Advanced BoostingBoosting di veicoli dal tablet: contratti dalla classe D alla S+, crew e coda in tempo reale.Vedi script →

Continua a leggere