Chaque fois qu'un client peut déclencher une action sur le serveur, il y a un potentiel d'abus. Bien que RemoteEvents et RemoteFunctions constituent le vecteur d'attaque le plus courant ici, d'autres instances comme ProximityPrompt sont également susceptibles. Cette section couvre comment sécuriser cette frontière.
Validation des entrées distantes
Chaque pièce de données envoyée par un client doit être validée par le serveur avant d'être utilisée. Appliquez ces couches de validation en fonction de ce que fait le réseau.
Validation de contexte/permisssion
Ceci est nécessaire pour les réseaux qui affectent l'état de l'expérience, la progression ou d'autres joueurs. Le serveur doit vérifier si le joueur a les permissions nécessaires pour faire la demande. Par exemple, le joueur est-il suffisamment près d'un magasin pour acheter quelque chose ? A-t-il une clé requise pour ouvrir une porte ? Son personnage est-il en vie ?
La validation est nécessaire pour les réseaux qui :
- Accordent des récompenses ou modifient la progression du joueur
- Affectent d'autres joueurs ou l'état partagé de l'expérience
- Effectuent des actions avec des exigences de distance, de temps ou de permission (par exemple, la distance d'un magasin)
Validation de type et de structure
Une autre attaque que les exploitants pourraient lancer consiste à envoyer des types techniquement valides mais à les rendre extrêmement grands, longs ou autrement mal formés. Par exemple, si le serveur doit effectuer une opération coûteuse sur une chaîne qui évolue avec la longueur, un exploitant pourrait envoyer une chaîne incroyablement grande ou malformée pour ralentir le serveur.
Une autre attaque courante que les exploitants peuvent utiliser implique l'envoi de tables à la place d'une Instance. Des charges utiles complexes peuvent imiter ce qui serait une référence d'objet autrement ordinaire.
Par exemple, dans un système de magasin dans l'expérience où les données d'éléments comme les prix sont stockées dans des objets NumberValue, un exploitant peut contourner toutes les autres vérifications en réalisant ce qui suit :
LocalScript in StarterPlayerScriptslocal ReplicatedStorage = game:GetService("ReplicatedStorage")local itemDataFolder = ReplicatedStorage:WaitForChild("ItemData")local buyItemEvent = ReplicatedStorage:WaitForChild("BuyItemEvent")local payload = {Name = "Ultra Blade",ClassName = "Folder",Parent = itemDataFolder,Price = {Name = "Price",ClassName = "NumberValue",Value = 0, -- Des valeurs négatives pourraient également être utilisées, entraînant la remise de monnaie plutôt que son retrait !},}-- Envoyer une charge utile malveillante au serveur (cette demande sera rejetée)print(buyItemEvent:InvokeServer(payload)) -- Affiche "false Invalid item provided"-- Envoyer un véritable objet au serveur (cela fonctionnera !)print(buyItemEvent:InvokeServer(itemDatafolder["Real Blade"])) -- Affiche "true" et la monnaie restante si l'achat réussit
Script in ServerScriptStorage
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local itemDataFolder = ReplicatedStorage:WaitForChild("ItemData")
local buyItemEvent = ReplicatedStorage:WaitForChild("BuyItemEvent")
local function buyItem(player, item)
-- Vérifier si l'objet passé n'est pas falsifié et est dans le dossier ItemData
if typeof(item) ~= "Instance" or not item:IsDescendantOf(itemDataFolder) then
return false, "Invalid item provided"
end
-- Le serveur peut ensuite procéder au traitement de l'achat en fonction de l'exemple de flux ci-dessous
end
-- Lier "buyItem()" au rappel de la fonction distante
buyItemEvent.OnServerInvoke = buyItem
Validation des valeurs
En plus de valider les types et les données, vous devez valider les valeurs passées par RemoteEvents et RemoteFunctions, en vous assurant qu'elles sont valides et logiques dans le contexte demandé. Ceci est essentiel pour les réseaux qui gèrent des devises, des objets, des entrées numériques ou du contenu généré par des utilisateurs. Vous devez vous assurer que les valeurs sont dans les limites prévues (par exemple, qu'une quantité d'achat est supérieure à zéro) et que les identifiants ou noms fournis sont valides (par exemple, un itemId correspond à un véritable objet dans votre jeu).
Considérations spéciales pour les valeurs numériques : Tant inf que NaN sont des types de nombres valides, mais les deux peuvent causer de graves problèmes si un exploitant les envoie et qu'ils ne sont pas gérés correctement. Utilisez des fonctions comme celles-ci pour les détecter et les rejeter :
local function isNaN(n: number): boolean
-- NaN n'est jamais égal à lui-même
return n ~= n
end
local function isInf(n: number): boolean
-- Le nombre pourrait être -inf ou +inf
return math.abs(n) == math.huge
end
Le danger de NaN
Un exploitant peut envoyer NaN (Not a Number) comme argument. NaN est particulièrement dangereux car il est de type "nombre" mais échoue à toutes les comparaisons standard, lui permettant de contourner subtilement les contrôles logiques qui semblent sûrs. Regardons un système de trading vulnérable où un joueur fait une offre.
-- CODE SERVEUR VULNÉRABLE
local function onCreateTradeOffer(player, offeredGold)
-- 1. VÉRIFICATION DE TYPE : Cela passe ! typeof(NaN) est "number".
if typeof(offeredGold) ~= "number" then
return "Invalid offer"
end
-- 2. VÉRIFICATION DE PLAGE : C'est contourné !
-- (NaN < 0) est faux. (NaN > 1000000) est également faux. La vérification ne sert à rien.
if offeredGold < 0 or offeredGold > 1000000 then
return "Offer out of range"
end
-- 3. VÉRIFICATION D'INVENTAIRE : C'est contourné !
-- (NaN > player.Gold.Value) est faux.
if offeredGold > player.Gold.Value then
return "Not enough gold"
end
-- VULNÉRABILITÉ : Une offre de trading frauduleuse avec de l'or NaN est créée !
createTrade(player, {gold = offeredGold})
return "Trade offer created."
end
L'exploitant a réussi à créer une offre de trade sans avoir d'or, car chaque vérification a échoué silencieusement. De plus, cette valeur NaN unique empoisonnera désormais tout autre système qui essaie de l'utiliser et pourra même se propager à d'autres joueurs par l'offre.
Voici quelques exemples à haut niveau couvrant la conception sécurisée de fonctionnalités courantes dans le jeu.
Magasin dans l'expérience
Considérez un système de magasin dans l'expérience avec une interface utilisateur, par exemple un menu de sélection de produits avec un bouton "Acheter". Lorsque le bouton est pressé, vous pouvez invoquer une RemoteFunction entre le client et le serveur pour demander l'achat. Cependant, il est important que le serveur, le gestionnaire le plus fiable de l'expérience, confirme que l'utilisateur a suffisamment d'argent pour acheter l'article.

Ciblage des armes
Les scénarios de combat nécessitent une attention spéciale à la validation des valeurs, en particulier à travers le ciblage et la validation des coups.
Imaginez une expérience où un joueur peut tirer un rayon laser sur un autre joueur. Plutôt que le client de dire au serveur qui doit être touché, il devrait plutôt indiquer au serveur la position d'origine du tir et la partie/la position qu'il pense avoir touchée. Le serveur peut alors valider les éléments suivants :
- La position à partir de laquelle le client signale avoir tiré est proche du personnage du joueur sur le serveur. Notez que le serveur et le client différeront légèrement en raison de la latence, donc une tolérance supplémentaire devra être appliquée.
- La position que le client signale avoir touchée est raisonnablement proche de la position de la pièce que le client signale avoir touchée, sur le serveur.
- Il n'y a aucune obstruction statique entre la position que le client signale avoir tiré et la position que le client signale avoir touchée. Cette vérification garantit qu'un client ne tente pas de tirer à travers des murs. Notez que cela ne devrait vérifier que la géométrie statique pour éviter que des tirs valides ne soient rejetés en raison de la latence.
De plus, vous voudrez peut-être mettre en œuvre d'autres validations côté serveur comme suit :
- Suivez quand le joueur a tiré pour la dernière fois son arme et vérifiez qu'il ne tire pas trop rapidement.
- Suivez la quantité de munitions de chaque joueur sur le serveur et confirmez qu'un joueur tirant a suffisamment de munitions pour exécuter l'attaque de l'arme.
- Si vous avez mis en œuvre des équipes ou un système de combat "joueurs contre bots", confirmez que le personnage touché est un ennemi, pas un coéquipier.
- Confirmez que le joueur touché est en vie.
- Stockez l'état de l'arme et du joueur sur le serveur et confirmez qu'un joueur tirant n'est pas bloqué par une action en cours telle que recharger ou un état comme sprinter.
Limitation de fréquence
Chaque fois que la logique côté serveur peut être déclenchée par le client (via des réseaux, des événements Touched, des invites de proximité, des ClickDetectors, etc.), il existe une possibilité que cette logique puisse être spammée par des exploitants ou même des utilisateurs légitimes. La limitation de fréquence contrôle la fréquence à laquelle ces actions peuvent être exécutées, empêchant les abus et la surcharge du système. Bien qu'il soit pratique (et parfois nécessaire) d'implémenter des limites de fréquence côté client, ne comptez jamais uniquement sur une limitation de fréquence côté client.
Pour toute logique côté serveur qui peut être déclenchée par un client, pensez toujours au taux maximum auquel cette logique devrait fonctionner. De nombreuses actions côté serveur doivent avoir leur fréquence limitée pour éviter les abus ou les échecs, comme :
- Appels à l'API Roblox : Les API backend comme DataStoreService et BadgeService ont des limites de fréquence intégrées qui provoqueront l'échec de vos demandes si elles sont dépassées.
- Opérations coûteuses en calcul : Actions qui consomment des ressources serveur significatives ou impactent d'autres clients.
- Exemple serveur : clonage de gros modèles depuis ServerStorage, même s'ils ne sont jamais répliqués aux clients.
- Exemple client : instructing tous les clients connectés à mettre à jour leur GUI simultanément.
- Mécanismes exploitables : Actions qui pourraient être abusées si réalisées rapidement, comme accorder l'invulnérabilité, téléporter des joueurs ou attribuer de la monnaie.
Exemple de seau de jetons
Une approche robuste et courante pour la limitation de fréquence est l'algorithme seau de jetons. Imaginez que chaque utilisateur a un seau qui peut contenir un certain nombre de jetons. Pour effectuer une action, l'utilisateur doit dépenser un jeton. Le seau se remplira avec de nouveaux jetons à un rythme régulier. Cette méthode permet des pics d'actions à court terme lorsque cela est nécessaire, mais empêche le spam soutenu en imposant une fréquence moyenne dans le temps.
--!strict
-- Module situé dans ServerScriptService
type UserId = number
type Bucket = {
tokens: number,
last: number,
}
type TokenBucketT = {
capacity: number,
refillPerSecond: number,
buckets: { [UserId]: Bucket },
allow: (self: TokenBucketT, userId: UserId) -> boolean,
}
local TokenBucket = {}
TokenBucket.__index = TokenBucket
-- Crée un limiteur permettant au maximum `capacity` événements, se remplissant à nouveau à un rythme de
-- `capacity / windowSeconds` jetons par seconde.
function TokenBucket.new(capacity: number, windowSeconds: number): TokenBucketT
assert(capacity >= 1, "capacity must be >= 1")
assert(windowSeconds > 0, "windowSeconds must be > 0")
local self: TokenBucketT = {
capacity = capacity,
refillPerSecond = capacity / windowSeconds,
buckets = {},
}
return (setmetatable(self, TokenBucket) :: any) :: TokenBucketT
end
local function refill(b: Bucket, now: number, cap: number, rate: number)
local elapsed = now - b.last
if elapsed > 0 then
b.tokens = math.min(cap, b.tokens + elapsed * rate)
b.last = now
end
end
-- Retourne faux si l'utilisateur dépasserait sa limite
function TokenBucket:allow(userId: UserId): boolean
local now = time()
local b = self.buckets[userId]
if not b then
b = { tokens = self.capacity, last = now }
self.buckets[userId] = b
else
refill(b, now, self.capacity, self.refillPerSecond)
end
if b.tokens >= 1 then
b.tokens -= 1
return true
end
return false
end
return TokenBucket
Pour utiliser le module, vous devez d'abord créer une nouvelle instance de limiteur avec TokenBucket.new(capacity, windowSeconds). La capacity est le nombre maximum de demandes qu'un utilisateur peut faire en un rapide pic, et le windowSeconds détermine combien de temps il faut pour remplir tous ces jetons. Par exemple, TokenBucket.new(5, 10) crée un limiteur qui autorise des pics allant jusqu'à 5 demandes et se remplit d'un jeton toutes les deux secondes (10 secondes / 5 jetons).
Avant d'exécuter une logique protégée, appelez la méthode :allow(userId), refusant l'action si elle retourne faux. Il est également bon de pratique de nettoyer les données du seau de l'utilisateur lorsqu'il part pour éviter les fuites de mémoire.
L'exemple de script suivant démontre comment protéger un RemoteEvent utilisé pour un système de chat personnalisé contre les abus par spam d'utilisateurs.
-- Exemple d'utilisation dans un script serveur
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local TokenBucket = require(ServerScriptService.TokenBucket)
local ChatRemote = ReplicatedStorage:WaitForChild("ChatRemote") :: RemoteEvent
-- 5 messages toutes les 10 secondes (capacité 5, se remplit à 0.5 jetons/sec)
local chatLimiter = TokenBucket.new(5, 10)
ChatRemote.OnServerEvent:Connect(function(player: Player, message: string)
if not chatLimiter:allow(player.UserId) then
-- Trop rapide : abandonner la demande ou envoyer un avertissement de spam
return
end
-- Traiter le message (après d'autres validations)
broadcastMessage(player, message)
end)
-- Nettoyage pour éviter les fuites de mémoire
Players.PlayerRemoving:Connect(function(p: Player)
chatLimiter.buckets[p.UserId] = nil
end)
Sécurisation des instances déclenchées par le client
ProximityPrompts, ClickDetectors, et DragDetectors ne sont pas des réseaux, mais un exploitant peut déclencher leurs événements à n'importe quelle distance, à tout moment, en ignorant souvent des propriétés comme Enabled ou MaxActivationDistance. Ces objets doivent être sécurisés avec la même rigueur que les réseaux.
ProximityPrompt
- Un exploitant peut déclencher n'importe quel événement même si Enabled est faux sur le serveur.
- Enabled, MaxActivationDistance et RequiresLineOfSight peuvent être silencieusement modifiés sur le client, donc tout ProximityPrompt répliqué peut être vu et potentiellement interagi avec depuis n'importe quel emplacement.
- Le serveur acceptera les événements de maintien (comme PromptButtonHoldBegan) même si le HoldDuration côté serveur est nul. Le client peut manipuler le HoldDuration pour effectuer des interactions de manière anormalement rapide.
- Seul l'événement Triggered a une vérification de distance côté serveur. D'autres événements (comme PromptButtonHoldBegan et TriggerEnded) n'ont aucune vérification de distance, et peuvent être envoyés arbitrairement par n'importe quel client.
ClickDetector
- Il n'y a aucune vérification sur le serveur pour des événements.
- Les exploitants peuvent reproduire n'importe quel événement à n'importe quelle distance, même si le ClickDetector est désactivé (MaxActivationDistance de 0 ou DragDetector.Enabled faux).
- Les événements peuvent être reproduits même si le ClickDetector n'est pas parenté à quelque chose de cliquable.
DragDetector
- Pour les événements hérités de ClickDetector, il n'y a pas de vérifications.
- Pour les événements de glissement, les propriétés Enabled et PermissionPolicy sont vérifiées et respectées par le serveur. Toutes les autres propriétés ne sont pas vérifiées.
Lorsque l'un de ces événements est déclenché, votre script serveur doit effectuer sa propre validation avant d'agir :
- Vérifiez s'il est activé : Assurez-vous que l'instance est censée être activée en vérifiant ses propriétés (comme Enabled, HoldDuration, RunLocally, etc.) sur le serveur. Cette étape n'est pas nécessaire si l'objet est toujours activé.
- Vérifiez l'état du joueur : Assurez-vous que le joueur peut effectuer l'action, compte tenu de l'état actuel du joueur. Le personnage du joueur est-il dans le monde ? Son personnage est-il suffisamment près de l'objet ? Le joueur doit-il être en vie pour interagir avec cet objet ? Vérifiez tout autre état du joueur maintenu par votre expérience sur le serveur au besoin.
- Appliquez une limitation de fréquence : Comme couvert dans Limitation de fréquence, appliquez des temps d'attente sur le client et imposez des limites de fréquence sur le serveur pour éviter que ces événements ne soient spammés et abusés. Pour les interactions qui doivent prendre un minimum de temps, assurez-vous que les clients ne les complètent pas trop rapidement.
Note sur la propriété réseau : Par défaut, si l'un de ces objets est enfant d'une partie non ancrée ou d'un assemblage non ancré, un exploitant peut prendre la propriété réseau des parties parentes et les déplacer directement vers leur personnage, contournant les vérifications de distance. Pour les actions critiques, utilisez des parties ancrées ou l'API de propriété réseau avec la validation nécessaire côté serveur.
RemoteEvents et RemoteFunctions
Limitez la portée et l'impact des RemoteFunctions et RemoteEvents. Faites très attention à charger dynamiquement des ressources (même des textures ou des sons) ou à exécuter du code de l'expérience (surtout via require) sur la base des arguments des fonctions/événements distants. Évitez de mettre en œuvre des réseaux permettant à un client de spécifier un chemin arbitraire ou une référence d'instance pour que le serveur supprime ou modifie, même si de telles modifications semblent triviales. Les réseaux qui peuvent changer l'état d'une instance arbitraire ou effectuer des modifications larges dans l'arborescence DataModel peuvent souvent être chaînés avec d'autres bugs pour avoir un impact sévère sur l'état ou la logique globale. Vérifiez non seulement le type des arguments d'instance, mais aussi la classe et l'emplacement ou la structure attendue dans le DataModel.
Communication client à client
Certains réseaux sont conçus pour permettre à un client de déclencher des effets sur d'autres clients. Cela se produit lorsqu'un client envoie un réseau au serveur, qui utilise ensuite RemoteEvent:FireAllClients(), RemoteEvent:FireClient() ou des méthodes similaires pour relayer des informations. Ce modèle est dangereux s'il n'est pas correctement sécurisé - le serveur doit être un gardien, pas juste un relais.
Imaginez un scénario où les joueurs peuvent lancer un sort de foudre. Lorsqu'il est lancé, les joueurs à proximité voient l'éclair, entendent le bruit et ressentent un tremblement de caméra.
Un script serveur vulnérable pourrait ressembler à ceci :
Code serveur vulnérable (script dans ServerScriptStorage)
local castLightningEvent = game.ReplicatedStorage.CastLightning
-- Ce script serveur relaie le message à tout le monde sans validation
castLightningEvent.OnServerEvent:Connect(function(player, strikePosition)
castLightningEvent:FireAllClients(strikePosition)
end)
Code effect client
local castLightningEvent = game.ReplicatedStorage.CastLightning
-- Créer des effets visuels et sonores sur le client
local function createLightningEffect(strikePosition)
-- Code pour secouer la caméra
-- Code pour jouer un son fort de foudre
-- Code pour créer un éclair visuel
print("L'éclair frappe à : " .. tostring(strikePosition))
end
-- Lorsque le serveur diffuse l'événement, ce client exécute l'effet
castLightningEvent.OnClientEvent:Connect(createLightningEffect)
Un exploitant peut abuser de ce système vulnérable en :
- Spammant : Appelant le réseau des centaines de fois par seconde, provoquant des effets continus qui rendent l'expérience injouable.
- Données invalides : En envoyant nil, NaN ou de mauvaises types qui provoquent des erreurs de script sur tous les clients.
- Utilisation non autorisée : Lançant des sorts qu'ils n'ont pas débloqués ou dont ils n'ont pas les ressources.
Le serveur doit être un gardien, pas un relais. Avant de diffuser, il doit valider la demande et appliquer une limitation de fréquence par joueur. Voici un exemple de certaines méthodes qui pourraient être employées pour valider la demande.
Code serveur sécurisé
local castLightningEvent = game.ReplicatedStorage.CastLightning
local playerCooldowns = {}
local COOLDOWN_TIME = 3
castLightningEvent.OnServerEvent:Connect(function(player, strikePosition)
-- 1. Validation de type
if typeof(strikePosition) ~= "Vector3" then
return -- Type invalide, rejeter silencieusement
end
-- 2. Vérification de NaN (NaN ~= NaN est le seul moyen de le détecter)
if strikePosition.X ~= strikePosition.X then
return -- Contient NaN, rejet
end
-- 3. Exemple de limitation de fréquence
local lastCast = playerCooldowns[player] or 0
if tick() - lastCast < COOLDOWN_TIME then
return -- Toujours en cooldown
end
-- 4. Exemple de vérification de permission
if not player:GetAttribute("HasLightningSpell") then
return -- Le joueur n'a pas ce sort
end
-- 5. Exemple de validation de distance
local character = player.Character
local humanoidRootPart = character and character:FindFirstChild("HumanoidRootPart")
if not humanoidRootPart then
return
end
local distance = (humanoidRootPart.Position - strikePosition).Magnitude
if distance > 100 then
return -- Hors de portée
end
-- Toutes les vérifications sont passées - sûr de diffuser
playerCooldowns[player] = tick()
castLightningEvent:FireAllClients(player, strikePosition)
end)
En mettant en œuvre ces vérifications, il devient plus difficile pour un exploitant de gâcher l'expérience des autres joueurs. Le serveur agit comme une barrière protectrice, garantissant que seules des actions valides et autorisées sont diffusées à d'autres clients.
Considérations spéciales
Voici quelques cas supplémentaires que vous devriez envisager et qui pourraient nécessiter un traitement spécialisé.
Manipulation du magasin de données
Dans les expériences utilisant DataStoreService pour sauvegarder les données des joueurs, les exploitants peuvent tirer parti de données invalides, de conditions de course pour corrompre les sauvegardes ou dupliquer des éléments dans un DataStore. Cela est particulièrement problématique dans des expériences avec trading d'objets, marchés et systèmes de monnaie.
Assurez-vous que toutes les actions effectuées via une RemoteEvent ou RemoteFunction qui affectent les données des joueurs avec une entrée client soient assainies en fonction de ce qui suit :
- Les valeurs d'instance ne peuvent pas être sérialisées dans un DataStore et échoueront. Utilisez une validation de type pour éviter cela.
- Les DataStores ont des limites de données. Les chaînes de longueur arbitraire devraient être vérifiées et/ou limitées pour éviter cela, en veillant à ce que des clés arbitraires illimitées ne puissent pas être ajoutées aux tables par le client.
- Les indices de table ne peuvent pas être NaN ou nil. Itérez sur toutes les tables passées par le client et vérifiez que tous les indices sont valides.
- Les DataStores ne peuvent accepter que des caractères UTF-8 valides, vous devez donc assainir toutes les chaînes fournies par le client via utf8.len() pour vous assurer qu'elles sont valides. utf8.len() renverra la longueur d'une chaîne, considérant que les caractères unicode sont un seul caractère ; si un caractère UTF-8 invalide est rencontré, il renverra nil et la position du caractère invalide. Notez que des chaînes UTF-8 invalides peuvent également être présentes dans les tables en tant que clés et valeurs.
- Exploits de nombres infinis/NaN - Les exploitants peuvent envoyer des valeurs comme -1/0, 0/0 ou math.huge pour cause de monnaie infinie ou de rupture de calculs. Validez toujours en utilisant les méthodes décrites dans le danger de NaN.
Des conditions de course peuvent se produire lorsque des joueurs manipulent des temps entre opérations pour dupliquer des objets ou corrompre des données.
Exemple d'exploit de trading : Un joueur initie un trade, envoie son objet à un autre joueur, puis quitte immédiatement le jeu. Si le trade se termine mais que leur sauvegarde DataStore échoue en raison de données invalides, ils rejoignent avec leurs objets d'origine pendant que l'autre joueur conserve les objets échangés - résultant en duplication.
Stratégies de prévention :
- Validez toutes les données avant toute opération de trade.
- Utilisez des patterns de type transaction où les données de tous les joueurs sont validées avant de valider les modifications.
- Implémentez une gestion des erreurs appropriée qui revient sur toutes les modifications si une partie échoue.
MarketplaceService
Pour les interactions impliquant MarketplaceService, telles que des passes ou des produits développeurs, toutes les validations d'achat et de granting d'objet doivent se dérouler sur le serveur. Plus précisément, utilisez le rappel ProcessReceipt pour valider en toute sécurité les reçus d'achat. Ne faites jamais confiance à un signal côté client, tel que PromptProductPurchaseFinished, pour confirmer un achat sans vérification côté serveur, car ceux-ci peuvent être usurpés. Assurez-vous que votre fonction ProcessReceipt vérifie minutieusement les détails du reçu et ne donne des objets ou de la monnaie qu'après avoir confirmé une transaction légitime avec les serveurs Roblox, et soyez prêt à gérer des cas où un produit a déjà été accordé pour un reçu donné.