games / fivem
Économie serveur et détection de fraude
Explication
Ce que vous allez apprendre
- Identifier une race condition sur une opération lire-puis-écrire de solde
- Utiliser une transaction SQL avec verrou (
FOR UPDATE) pour rendre une opération atomique - Distinguer une réponse "ignorer silencieusement" d'une réponse "kick + log" selon la gravité
- Mettre en place une détection d'anomalie par seuil statistique sur une fenêtre glissante
- Comprendre pourquoi l'économie est la cible numéro un des tentatives de triche
Dans quel contexte ?
Deux clics rapprochés sur "vendre" dans l'interface d'un joueur déclenchent presque simultanément deux requêtes de vente du même objet. Sans protection, chacune lit le même solde de départ avant que l'autre n'ait fini d'écrire sa mise à jour — un joueur peut alors dupliquer de l'argent sans même chercher activement à exploiter une faille technique complexe. Cette leçon montre comment une transaction SQL avec verrou élimine structurellement ce risque.
| Anti-pattern | Risque | Correction |
|---|---|---|
| Lire solde, calculer, réécrire en plusieurs étapes | Race condition, duplication d'argent | Transaction SQL avec FOR UPDATE |
| Ignorer silencieusement une donnée incohérente | Encourage les tentatives répétées | Kick + log immédiat |
| Aucune détection statistique | Faille inconnue non détectée | Seuils sur fenêtre glissante |
Piège dangereux
Lire un solde, le modifier en mémoire, puis le réécrire en plusieurs étapes séparées (sans transaction) est exploitable dès que deux requêtes concernant le même joueur arrivent presque simultanément : c'est la cause la plus fréquente de duplication d'argent sur les serveurs RP.
L'argent, la cible numéro un des tricheurs
Sur un serveur roleplay, l'économie est presque toujours le système le plus attaqué : c'est là que se concentre la valeur perçue du jeu par les joueurs, et donc la plus forte incitation à tricher. Cette leçon approfondit un problème déjà évoqué dans la leçon Sécurité (leçon 16), mais spécifiquement appliqué à l'argent, où les conséquences d'une faille sont souvent les plus visibles et les plus dommageables pour l'équilibre du serveur.
Le danger invisible d'une "race condition"
Un piège subtil, difficile à repérer en testant seul : si deux requêtes concernant le même joueur arrivent presque simultanément (par exemple deux clics rapides sur "vendre"), chacune peut lire le solde AVANT que l'autre ait fini de l'écrire. Résultat : les deux mises à jour se basent sur la même valeur de départ, et l'une des deux augmentations d'argent est purement et simplement perdue — ou pire, dupliquée selon l'ordre exact des opérations. C'est ce qu'on appelle une "race condition" (condition de course).
La transaction SQL avec verrou, la vraie solution
La commande SQL FOR UPDATE verrouille la ligne concernée dès sa lecture, empêchant toute autre requête de la lire ou modifier tant que la transaction en cours n'est pas terminée. C'est la garantie technique, au niveau de la base de données elle-même, qu'une opération de lecture-puis-écriture reste réellement indivisible, même face à des requêtes simultanées.
Un comportement anormal mérite une vraie réaction
Recevoir une donnée manifestement incohérente (une quantité négative, un type de donnée inattendu) ne devrait jamais être traité comme une simple erreur à ignorer silencieusement : c'est un signal fort qu'un client a été altéré. Réagir fermement (jusqu'au kick) plutôt que de simplement abandonner l'opération dissuade activement les tentatives répétées d'exploitation.
Détecter ce qu'on n'a pas anticipé
Aucune règle fixe ne peut couvrir à l'avance toutes les formes possibles d'exploitation d'un système économique. La détection par seuils statistiques (une quantité d'argent générée anormalement élevée sur une courte période) permet de repérer des comportements suspects même quand la faille exacte exploitée n'a pas encore été identifiée par l'équipe de développement.
Commandes & code
Économie serveur et détection de fraude
Toute transaction d'argent doit être atomique et auditée côté serveur : c'est la source n°1 de duplication et de triche sur les serveurs RP.
-- MAUVAIS : lire le solde, calculer, réécrire, en plusieurs étapes non protégées (race condition exploitable)
RegisterNetEvent('technologik:sellItem', function(itemId, quantity)
local src = source
local price = GetItemPrice(itemId) * quantity
local balance = GetPlayerBalance(src) -- lecture
-- ... si deux requêtes arrivent simultanément ici, les deux lisent le MÊME solde avant écriture ...
SetPlayerBalance(src, balance + price) -- écriture non atomique = duplication d'argent possible
end)-- BON : transaction SQL atomique avec verrou, l'opération lecture+écriture est indivisible
-- (via oxmysql, exécuté côté serveur uniquement, jamais confiance dans un montant envoyé par le client)
START TRANSACTION;
SELECT balance FROM players WHERE identifier = ? FOR UPDATE; -- verrouille la ligne jusqu'au COMMIT
UPDATE players SET balance = balance + ? WHERE identifier = ?;
COMMIT;-- Défense en profondeur : ne JAMAIS faire confiance au prix envoyé par le client, toujours revalider serveur
RegisterNetEvent('technologik:sellItem', function(itemId, quantity)
local src = source
-- 1. Valider les entrées (types, bornes) avant toute logique métier
if type(itemId) ~= 'string' or type(quantity) ~= 'number' or quantity <= 0 or quantity > 100 then
DropPlayer(src, 'Données de transaction invalides') -- comportement anormal = kick, pas juste ignorer
return
end
-- 2. Le prix vient TOUJOURS d'une table serveur, jamais d'un paramètre client
local itemDef = ServerItemsConfig[itemId]
if not itemDef then return end
-- 3. Vérifier que le joueur possède réellement la quantité déclarée AVANT de créditer
local owned = GetPlayerItemCount(src, itemId)
if owned < quantity then
LogSuspiciousActivity(src, ('tentative de vente de %dx %s alors que %d possédés'):format(quantity, itemId, owned))
return
end
local totalPrice = itemDef.sellPrice * quantity
RemovePlayerItem(src, itemId, quantity)
AddPlayerBalanceAtomic(src, totalPrice) -- passe par la transaction SQL atomique ci-dessus
end)-- Détection d'anomalies : surveiller les patterns statistiquement improbables plutôt que des règles figées
local RecentTransactions = {} -- [source] = { {amount, timestamp}, ... }
local function trackTransaction(src, amount)
RecentTransactions[src] = RecentTransactions[src] or {}
table.insert(RecentTransactions[src], { amount = amount, ts = GetGameTimer() })
-- Ne garder que les 60 dernières secondes de transactions pour ce joueur
local cutoff = GetGameTimer() - 60000
local recent = {}
local total = 0
for _, tx in ipairs(RecentTransactions[src]) do
if tx.ts >= cutoff then
table.insert(recent, tx)
total = total + tx.amount
end
end
RecentTransactions[src] = recent
-- Seuil configurable : plus de 500k générés en 60s = très probablement un exploit ou un script tiers
if total > 500000 then
LogSuspiciousActivity(src, ('pic économique anormal: %d généré en 60s (%d transactions)'):format(total, #recent))
FlagPlayerForReview(src, 'economy_spike')
end
endRésumé
- Toute opération solde/inventaire doit passer par une transaction SQL atomique (
FOR UPDATE), jamais un lire-puis-écrire naïf. - Le prix et la quantité d'une transaction doivent toujours être revalidés côté serveur, jamais pris tels quels du client.
- Un comportement anormal (donnée invalide, quantité incohérente) mérite un kick/log immédiat, pas un simple
returnsilencieux. - La détection de fraude économique se base sur des seuils statistiques (pic anormal sur une fenêtre glissante), pas seulement des règles unitaires.
Exercices pratiques
Mission : enquêter sur une duplication d'argent
Objectif : Reconstituer précisément une race condition économique, la corriger avec une transaction verrouillée, puis calibrer la sévérité de la réponse à un comportement anormal.
Contexte
Sur technologik_rp, plusieurs joueurs signalent au support un solde d'argent bien supérieur à ce qu'ils auraient dû gagner. L'équipe retrouve dans les logs deux appels à technologik:sellItem pour le même objet, envoyés par le même joueur à quelques millisecondes d'écart, avec le code montré en anti-pattern dans la leçon (lecture du solde, puis écriture séparée, sans transaction).