Passer au contenu principal
Version: latest - a1.169.2 ⚖️

Ne jamais faire confiance au client

Comment protéger votre mode de jeu des tricheurs en validant tout ce que les clients envoient au serveur.

Pourquoi ?​

Tout ce qui tourne sur le client peut être modifié par un joueur malveillant : il peut appeler n'importe quel Remote Event, à n'importe quel moment, avec n'importe quels paramètres, autant de fois qu'il le veut. Les scripts client sont téléchargés sur son ordinateur : il peut donc aussi les lire pour savoir quels événements votre serveur écoute.

Autrement dit, le serveur ne doit jamais faire aveuglément confiance à ce qui arrive via Events.SubscribeRemote ou Entity.SubscribeRemote. Par exemple, ceci est dangereux :

Server/Index.lua
-- ❌ DON'T: any player can give themselves any amount of money, or damage anyone from anywhere
Events.SubscribeRemote("BuyItem", function(player, item_name, price)
player:SetValue("money", player:GetValue("money", 0) - price)
GiveItem(player, item_name)
end)

Events.SubscribeRemote("HitEnemy", function(player, enemy, damage)
enemy:ApplyDamage(damage)
end)

Un tricheur pourrait appeler BuyItem avec un price négatif pour gagner de l'argent, ou HitEnemy avec damage = 99999 pour tuer tout le monde.

La règle d'or​

Le client ne doit envoyer que ce que le joueur veut faire (l'intention), et c'est le serveur qui décide si c'est autorisé et ce qui se passe, à partir des données qu'il connaît déjà :

Server/Index.lua
-- The prices are defined on the server, the client can't change them
local ITEM_PRICES = {
medkit = 100,
armor = 250,
}

-- ✅ DO: the client only says which item it wants
Events.SubscribeRemote("BuyItem", function(player, item_name)
-- Validates the type and if the item exists
if (type(item_name) ~= "string") then return end
local price = ITEM_PRICES[item_name]
if (not price) then return end

-- Validates if the player can afford it
local money = player:GetValue("money", 0)
if (money < price) then return end

player:SetValue("money", money - price, true)
GiveItem(player, item_name)
end)

Ce qu'il faut valider​

Pour chaque Remote Event reçu sur le serveur, posez-vous ces questions :

  • Types : chaque paramètre est-il du type attendu ? Un tricheur peut envoyer une table là où vous attendez un nombre. Utilisez type(value) et, pour les entités, value:IsValid() et value:IsA(SomeClass).
  • Plages : les nombres sont-ils dans la plage attendue (pas de montants négatifs, pas de valeurs énormes, pas de NaN) ?
  • Propriété : ce joueur a-t-il le droit d'agir sur cette entité ? Exemple. est-ce son propre Character, ou un Prop qu'il a fait apparaître ?
  • Distance : le joueur est-il assez proche pour faire ça ? Comparez la position de son Character avec celle de la cible.
  • État : l'action est-elle possible en ce moment ? Exemple. le joueur est-il vivant, la manche est-elle en cours, a-t-il l'outil en main ?
  • Fréquence : le joueur l'envoie-t-il trop souvent ? Limitez le nombre de fois par seconde qu'une action peut se produire.
Server/Index.lua
-- Remembers the last time each player used the action
local last_use = {}

Events.SubscribeRemote("UseAbility", function(player, target)
-- State: must be controlling a living Character
local character = player:GetControlledCharacter()
if (not character or character:IsDead()) then return end

-- Types: the target must be a valid Character
if (not target or not target:IsValid() or not target:IsA(Character)) then return end

-- Distance: must be closer than 10 meters (1000 units)
if (character:GetLocation():Distance(target:GetLocation()) > 1000) then return end

-- Rate: once every 2 seconds
local now = Server.GetTime() -- milliseconds
if (last_use[player] and now - last_use[player] < 2000) then return end
last_use[player] = now

-- The server decides the damage
target:ApplyDamage(25, "", DamageType.Unknown, Vector(), player, character)
end)

-- Cleans up when the player leaves
Player.Subscribe("Destroy", function(player)
last_use[player] = nil
end)

Autres conseils​

  • Gardez les secrets sur le serveur : les clés d'API, les identifiants de base de données et toute logique que les joueurs ne doivent pas lire doivent se trouver dans les fichiers Server/, qui ne sont jamais envoyés aux clients. Tout ce qui se trouve dans Client/ et Shared/ est téléchargé par les joueurs.
  • Privilégiez les événements côté serveur : quand le jeu déclenche déjà un événement sur le serveur (par ex. Death, PickUp, EnterVehicle), utilisez-le au lieu de demander au client de vous prévenir.
  • Utilisez les événements Attempt* pour bloquer des actions sur le serveur, par ex. retournez false dans AttemptEnterVehicle pour empêcher un joueur de monter dans un véhicule.
  • Network Authority : le client qui est la Network Authority d'une entité simule sa physique, donc sa position peut être influencée par ce client. Ne vous fiez pas uniquement aux positions physiques pour décider de choses importantes, comme le vainqueur d'une course.
  • Expulsez ou bannissez les joueurs qui envoient clairement des données invalides avec player:Kick(reason) ou player:Ban(reason), mais attention aux faux positifs dus au lag.