Parent : [[INDEX-FIVEM-FRAMEWORK]]
FiveM - Architecture Server-Authoritative : State Bags et Anti-Cheat fondamental
Problème
Dans FiveM, le client est entièrement contrôlé par le joueur (moddable via Lua injecté, trainers, menus). Toute logique critique exécutée côté client est vulnérable au spoofing : santé, position, argent, inventaire, permissions. Sans architecture server-authoritative, un cheateur peut manipuler toutes ces données.
Contexte
FiveM est un framework GTA V multiplayer où :
- Le client exécute du Lua local (injectable/modifiable)
- Le serveur exécute du Lua server-side (contrôlé par l'hôte)
- Les events réseau transitent entre client et serveur
- Les state bags permettent une synchronisation server-authoritative native
Solution
1. State Bags : La source de vérité unique
-- Le serveur est la source de vérité
-- server.lua
local playerHealth = {}
-- State bag : synchronisé automatiquement client → serveur → autres clients
AddEventHandler('playerConnecting', function(_, _, deferrals)
local src = source
Player(src).state:set('health', 100, true) -- true = répliqué aux clients
end)
-- Le serveur valide TOUTE modification de santé
RegisterNetEvent('medical:requestHeal')
AddEventHandler('medical:requestHeal', function()
local src = source
local currentHealth = Player(src).state.health or 100
-- Validation métier : le joueur a-t-il le droit de se soigner ?
if not canHeal(src) then
print(('[%s] Tentative de heal non autorisée'):format(src))
return
end
-- Le serveur calcule et impose la nouvelle valeur
local newHealth = math.min(currentHealth + 20, 100)
Player(src).state:set('health', newHealth, true)
-- Le client reçoit l'ordre du serveur (pas l'inverse)
TriggerClientEvent('medical:setHealth', src, newHealth)
end)
2. Synchronisation positionnelle server-authoritative
RegisterNetEvent('player:heartbeat')
AddEventHandler('player:heartbeat', function(clientPos)
local src = source
local ped = GetPlayerPed(src)
local serverPos = GetEntityCoords(ped)
-- Tolérance : 10m (lag + interpolation) + vitesse max * deltaTime
local maxDistance = 10.0 + (50.0 * 0.5)
local dist = #(vector3(clientPos.x, clientPos.y, clientPos.z) - serverPos)
if dist > maxDistance then
print(('[%s] Teleport detecte : %.1fm'):format(src, dist))
SetEntityCoords(ped, serverPos.x, serverPos.y, serverPos.z, false, false, false, false)
Player(src).state:set('suspectedTeleport', true, false)
end
end)
3. Validation des actions critiques
function MedicalSystem:treatPatient(medicId, patientId, treatmentType)
if not GetPlayerName(medicId) or not GetPlayerName(patientId) then
return false, 'Joueur introuvable'
end
local medicJob = self:getPlayerJob(medicId)
if medicJob ~= 'ambulance' then
self:logCheat(medicId, 'treatPatient sans job EMS')
return false, 'Permission insuffisante'
end
local medicPos = GetEntityCoords(GetPlayerPed(medicId))
local patientPos = GetEntityCoords(GetPlayerPed(patientId))
if #(medicPos - patientPos) > 5.0 then
return false, 'Patient trop loin'
end
local patientHealth = Player(patientId).state.health or 100
if patientHealth >= 100 then
return false, 'Patient deja soigne'
end
local newHealth = math.min(patientHealth + Config.TreatmentHeal[treatmentType], 100)
Player(patientId).state:set('health', newHealth, true)
self:logAction(medicId, patientId, 'treat', treatmentType, newHealth)
return true, 'Traitement applique'
end
4. Rate limiting des events critiques
local EventRateLimiter = {}
EventRateLimiter.cooldowns = {}
function EventRateLimiter:check(src, eventName, maxPerSecond)
local key = src .. ':' .. eventName
local now = GetGameTimer()
if not self.cooldowns[key] then
self.cooldowns[key] = { count = 0, windowStart = now }
end
local window = self.cooldowns[key]
if now - window.windowStart > 1000 then
window.count = 0
window.windowStart = now
end
window.count = window.count + 1
if window.count > maxPerSecond then
return false
end
return true
end
Checklist de validation
- TOUTE donnée critique (sante, argent, position, inventaire) est stockee server-side
- State bags utilisees pour la synchronisation (
Player(src).state:set()) - Le client ne fait que REQUESTER, le serveur APPROUVE et EXECUTE
- Validation de distance server-side pour toutes les interactions physiques
- Validation de job/permission server-side (pas cote client)
- Rate limiting sur les events modifiant l'etat
- Logs d'audit pour les actions critiques (soins, arrestations, transactions)
- Le serveur peut override la position du client (
SetEntityCoords) - Pas de
TriggerServerEventavec des donnees non-validees (position, sante) - Anti-cheat server-side pour la vitesse de deplacement (speed hack)
Anti-pattern associe
[[KNOW-ANT-FIVEM-002-client-side-validation-only|KNOW-ANT-FIVEM-002]]— Client-side validation only
References
- FiveM Docs : State Bags https://docs.fivem.net/docs/scripting-reference/runtimes/lua/functions/global-state/
Liens connexes
- [[KNOW-ANT-FIVEM-002-client-side-validation-only|KNOW-ANT-FIVEM-002]]