Retour au cours

games / fivem

Sécurité : validation serveur et anti-triche

Leçon 161 exercice

Explication

Ce que vous allez apprendre

  • Identifier pourquoi un client FiveM ne peut jamais être considéré comme fiable
  • Recalculer côté serveur toute donnée ayant une conséquence économique ou de gameplay
  • Mettre en place un rate limiting basique contre le spam d'events
  • Valider strictement les types et les bornes de toute donnée reçue d'un client
  • Détecter un déplacement physiquement impossible (speedhack, téléportation)

Dans quel contexte ?

Un serveur RP fraîchement ouvert au public constate en quelques heures que plusieurs joueurs disposent de sommes d'argent totalement disproportionnées par rapport à leur temps de jeu. L'enquête révèle qu'un event serveur faisait confiance au prix envoyé directement par le client pour un achat — une faille exploitée en quelques minutes par des joueurs ayant modifié leur client. Cette leçon détaille précisément comment ce genre d'incident, très fréquent sur les serveurs débutants, se prévient dès la conception.

DonnéeDoit être validée où ?
Prix d'un objetToujours recalculé côté serveur, jamais reçu du client
Quantité déclarée par le clientRevérifiée contre l'état réel côté serveur
Fréquence d'un eventLimitée par un rate limiting explicite

Piège dangereux

Faire confiance à une valeur envoyée par le client pour une décision économique (RegisterNetEvent('shop:buy', function(itemName, price) ... end)) est une faille critique : un client modifié peut envoyer n'importe quelle valeur, y compris un prix de zéro.

Le principe qui traverse tout ce cours, enfin détaillé

Depuis la leçon sur le modèle client/serveur, une idée revient constamment en filigrane : ne jamais faire confiance au client. Cette leçon la traite enfin de façon frontale et systématique, car c'est très probablement le sujet le plus important pour quiconque veut faire tourner un serveur RP public sans le voir ruiné par la triche en quelques jours.

Pourquoi le client ne peut jamais être fiable

Un client FiveM tourne entièrement sur une machine que le joueur contrôle. Des outils existent pour modifier le comportement du jeu localement, intercepter et altérer les données envoyées, ou simuler des actions qui n'ont jamais réellement eu lieu dans le jeu. Concrètement, cela signifie qu'un event serveur peut recevoir absolument N'IMPORTE QUELLE valeur d'un client malveillant — y compris des valeurs qu'un client "honnête" n'enverrait jamais.

Toujours recalculer, jamais recevoir

La règle pratique qui en découle est simple à énoncer mais facile à oublier en pratique : toute donnée qui a une conséquence économique ou de gameplay réelle (un prix, une quantité, des dégâts) doit être RECALCULÉE ou relue depuis une source fiable côté serveur, jamais simplement acceptée telle quelle depuis ce que le client a envoyé.

Le rate limiting, une protection complémentaire

Même une fois les valeurs validées, un client peut chercher à exploiter une faille en déclenchant un event en boucle très rapidement (spam), par exemple pour tenter de dupliquer une action avant que le serveur n'ait eu le temps de mettre à jour son état. Limiter la fréquence à laquelle un même joueur peut déclencher un event précis ferme cette classe entière de vulnérabilités.

Une détection qui reste probabiliste

Les vérifications de plausibilité (comme une distance parcourue physiquement impossible en un temps donné) ne prouvent jamais une triche avec une certitude absolue — un pic de latence peut aussi en être la cause. C'est pour cela qu'on privilégie le logging systématique des cas suspects plutôt qu'une sanction automatique instantanée, laissant à un humain la décision finale.

Commandes & code

Sécurité : ne jamais faire confiance au client

Un client FiveM peut être modifié (mods, injections). Toute décision qui affecte le jeu doit être revalidée côté serveur.

lua
-- MAUVAIS : faire confiance au client pour le prix envoyé
RegisterNetEvent('shop:buy', function(itemName, price)
    -- le client peut envoyer n'importe quelle valeur, ex: price = 0
    RemoveMoney(source, price) -- FAILLE CRITIQUE : le joueur choisit son propre prix
    AddItem(source, itemName, 1)
end)
lua
-- BON : le serveur est la SEULE source de vérité pour les prix et les règles
RegisterNetEvent('shop:buy', function(itemName)
    local src = source
    local itemConfig = Config.Items[itemName]
    if not itemConfig then return end -- item inexistant, on ignore silencieusement

    local price = Config.ShopPrices[itemName] -- prix lu côté serveur, jamais depuis le client
    if not price then return end

    local playerMoney = GetPlayerMoney(src)
    if playerMoney < price then
        TriggerClientEvent('technologik:notify', src, 'Fonds insuffisants')
        return
    end

    RemoveMoney(src, price)
    AddItem(src, itemName, 1)
end)
lua
-- Rate limiting basique contre le spam d'events (protection anti-exploit générique)
local lastTrigger = {}

local function IsRateLimited(source, eventName, cooldownMs)
    local key = source .. ':' .. eventName
    local now = GetGameTimer()

    if lastTrigger[key] and (now - lastTrigger[key]) < cooldownMs then
        return true
    end

    lastTrigger[key] = now
    return false
end

RegisterNetEvent('shop:buy', function(itemName)
    local src = source
    if IsRateLimited(src, 'shop:buy', 500) then
        print(('[SECURITY] %s spam shop:buy'):format(src))
        return
    end
    -- ... logique d'achat validée
end)
lua
-- Validation stricte des types et des bornes de valeur
RegisterNetEvent('technologik:setWaypoint', function(x, y)
    if type(x) ~= 'number' or type(y) ~= 'number' then return end
    if x < -8000.0 or x > 8000.0 or y < -8000.0 or y > 8000.0 then return end -- hors carte = suspect
    -- ...
end)

-- Détection basique de déplacement impossible (speedhack / téléportation)
local lastCoords = {}

AddEventHandler('playerSpawned', function()
    lastCoords[source] = GetEntityCoords(GetPlayerPed(source))
end)

CreateThread(function()
    while true do
        Wait(2000)

        for _, playerId in ipairs(GetPlayers()) do
            local ped = GetPlayerPed(playerId)
            local coords = GetEntityCoords(ped)
            local last = lastCoords[playerId]

            if last then
                local distance = #(coords - last)
                if distance > 300.0 then -- déplacement impossible en 2 secondes
                    print(('[ANTICHEAT] %s téléportation suspecte: %.1fm'):format(playerId, distance))
                end
            end

            lastCoords[playerId] = coords
        end
    end
end)

Résumé

  • Toute donnée métier sensible (prix, quantité, dégâts) doit être décidée et lue côté serveur uniquement.
  • Le rate limiting protège contre le spam d'events, souvent utilisé pour dupliquer des ressources.
  • Une détection anti-triche basique (bornes, vitesse de déplacement) suffit à repérer une grande partie des abus courants.
  • Logger systématiquement les tentatives suspectes facilite l'investigation après coup.

Exercices pratiques

1 disponible
1

Mission : colmater complètement la faille de la boutique

Objectif : Corriger une faille de prix côté client, ajouter un rate limiting, et raisonner sur les limites d'une détection anti-triche probabiliste.

Contexte

L'audit de sécurité de technologik_shop a permis de corriger le event shop:buy pour qu'il ne reçoive plus le prix depuis le client, seulement itemName. L'équipe pense le problème résolu, mais un joueur parvient encore à acheter des dizaines d'objets en quelques secondes en spammant le bouton d'achat depuis un client modifié.

Résoudre l’exercice →