Sécuriser les événements serveur FiveM : arrêter les tricheurs d'abuser des événements réseau
Les tricheurs déclenchent vos événements serveur avec n'importe quelles valeurs. Comment valider sur le serveur : utiliser source, vérifier la distance, le travail et les refroidissements, et garder les prix dans un tableau serveur.
Un script de boutique sur beaucoup de serveurs ressemble à ceci, et c'est un bouton argent gratuit :
TriggerServerEvent('shop:buy', 'weapon_pistol', 1, -999999)Quiconque avec un menu de triche peut envoyer cette ligne. Le serveur exécute le gestionnaire, prend un prix négatif du joueur et ajoute joyeusement l'argent. Ce guide montre comment écrire des événements serveur qui ne font pas confiance au client.
La règle : le client n'est pas approuvé
Tout ce qui arrive dans un événement réseau provient de la machine d'un joueur, que ce joueur contrôle. Traitez chaque argument comme une entrée utilisateur sur un site web : il peut être manquant, du mauvais type, négatif, énorme ou tout simplement inventé.
Ce que le serveur peut approuver :
source, le joueur qui a déclenché l'événement. Le moteur le définit.- Les données que le serveur détient déjà : le travail du joueur, l'inventaire, l'argent, la position, et vos propres tableaux de configuration.
Ce qu'il ne doit jamais approuver : montants, prix, noms d'articles, IDs de joueurs, coordonnées, noms de travail et drapeaux "succès" envoyés par le client.
Un mauvais exemple
RegisterNetEvent('shop:buy', function(item, count, price)
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.removeMoney(price * count)
xPlayer.addInventoryItem(item, count)
end)Le client choisit l'article, la quantité et le prix. Un tricheur achète n'importe quoi pour rien, ou un compte négatif pour imprimer de l'argent.
Un bon exemple
Gardez les prix sur le serveur, validez les types, vérifiez où se trouve le joueur, puis agissez.
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)) sur le serveur nécessite OneSync activé. Si vous ne l'avez pas déjà fait, consultez activation de OneSync.
Pareil sur QBCore et QBox
Seule la recherche du joueur, le champ de travail et l'appel d'argent changent. La validation reste la même.
-- 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')Vérifications de travail et d'autorisation
Si un événement ne devrait fonctionner que pour un travail, vérifiez-le sur le serveur à partir des données du framework, et non pas à partir d'un argument.
-- 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 endPour les actions d'administrateur, vérifiez une permission ACE au lieu d'un drapeau client. Consultez permissions ACE :
if not IsPlayerAceAllowed(source, 'command.myadmin') then return endLes récompenses ont besoin de preuves côté serveur
Les événements comme job:finished ou mission:reward sont la cible classique. Un tricheur les déclenche simplement en boucle.
- Gardez le montant de la récompense dans un tableau serveur, jamais dans les arguments d'événement.
- Rappelez-vous sur le serveur que le joueur a commencé le travail, et ne payez que si cet état existe. Effacez-le quand vous payez.
- Ajoutez un temps minimum entre le début et la fin.
- Vérifiez que le joueur est près du point de livraison quand il finit.
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)D'autres habitudes qui aident
- N'utilisez jamais l'ID du joueur du client. Utilisez toujours
source. Un événement commegiveMoney(playerId, amount)laisse un tricheur payer n'importe qui. - N'exposez pas les événements dont vous n'avez pas besoin. Un événement enregistré uniquement avec
AddEventHandler, sansRegisterNetEvent, ne peut pas être déclenché à partir d'un client. - Ne lancez pas les chaînes client comme code. Ne passez jamais une valeur client à
load,ExecuteCommandou une chaîne SQL. Utilisez les requêtes paramétrées, comme dans le guide oxmysql. - Enregistrez les cas étranges. Quand une vérification échoue, imprimez le joueur et le nom de l'événement. Un motif d'échecs est comment vous trouvez les tricheurs. Bannir de txAdmin, consultez modération avec txAdmin.
Attention : cacher le nom de l'événement, l'offusquer ou ajouter un « jeton » côté client ne le protège pas. Le client peut lire tout cela. Seules les vérifications sur le serveur comptent.
Liste de contrôle
| Symptôme | Correction |
|---|---|
| Le client envoie le prix ou le montant | Gardez un tableau de prix sur le serveur et calculez le total là |
| Le client envoie un ID de joueur | Utilisez source à la place |
| Le nom de l'article vient du client | Acceptez uniquement les noms trouvés dans un tableau serveur |
| Montants négatifs ou énormes | Vérifiez type, math.floor, et un minimum et un maximum |
| Événement déclenché de l'autre côté de la carte | Comparez GetEntityCoords(GetPlayerPed(source)) avec la position cible |
| Événement spammé en boucle | Refroidissement par joueur, effacé dans playerDropped |
| Action réservée au travail | Lisez le travail du framework sur le serveur |
| Événement de récompense abusé | Enregistrez l'état du travail sur le serveur et payez un montant fixe une seule fois |
Réponses rapides
Un joueur peut-il vraiment déclencher mes événements serveur ?
Oui. N'importe quel événement enregistré avec RegisterNetEvent peut être déclenché par un client modifié avec n'importe quels arguments. Le serveur ne peut pas faire la différence entre un vrai appel de script et un faux, donc il doit valider tout.
Est-ce sûr d'utiliser un ID de joueur envoyé par le client ?
Non. Sur le serveur, source est défini par le moteur et ne peut pas être contrefait. Un ID de joueur, un nom ou un identifiant envoyé comme argument peut l'être, donc ne l'utilisez jamais pour décider qui obtient de l'argent ou qui est puni.
Ai-je besoin d'un antitricheur si je valide mes événements ?
La validation est la base et arrête la plupart des abus de vos propres scripts. Un antitricheur ajoute la détection pour les choses que les événements ne peuvent pas voir, comme des menus injectés. Consultez [les bases de l'antitricheur FiveM](/blog/fivem-anticheat-basics).
Des scripts sans ce problème
Shop CreatorCréez un magasin en moins d’une minute — propriétaires, employés, coffres et braquages inclus.Voir le script →
Item Creator V2Créez des items utilisables avec animations, props, effets et plus — sans écrire une ligne de code.Voir le script →
Advanced BoostingDu boosting de véhicules piloté par tablette : contrats de classe D à S+, crews et file d’attente en direct.Voir le script →