Retour au cours

games / fivem

Économie serveur et détection de fraude

Leçon 201 exercice

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-patternRisqueCorrection
Lire solde, calculer, réécrire en plusieurs étapesRace condition, duplication d'argentTransaction SQL avec FOR UPDATE
Ignorer silencieusement une donnée incohérenteEncourage les tentatives répétéesKick + log immédiat
Aucune détection statistiqueFaille inconnue non détectéeSeuils 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.

lua
-- 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)
sql
-- 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;
lua
-- 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)
lua
-- 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
end

Ré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 return silencieux.
  • 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

1 disponible
1

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

Résoudre l’exercice →