Sicurezza del confine client-server

*Questo contenuto è tradotto usando AI (Beta) e potrebbe contenere errori. Per visualizzare questa pagina in inglese, clicca qui.

Ogni volta che un client può attivare un'azione sul server, c'è il potenziale per abusi. Mentre RemoteEvents e RemoteFunctions sono i vettori di attacco più comuni qui, altre istanze come ProximityPrompt sono suscettibili. Questa sezione tratta di come proteggere questo confine.

Validazione degli input remoti

Ogni pezzo di dati inviato da un client deve essere convalidato dal server prima di essere utilizzato. Applica questi strati di validazione in base a ciò che fa il remoto.

Validazione del contesto/permissibilità

Questo è necessario per i remoti che influenzano lo stato del gioco, la progressione o altri giocatori. Il server deve verificare se il giocatore ha le necessarie autorizzazioni per effettuare la richiesta. Ad esempio, il giocatore è abbastanza vicino a un negozio per acquistare qualcosa? Ha una chiave necessaria per aprire una porta? Il suo personaggio è vivo?

La validazione è necessaria per i remoti che:

  • Concedono ricompense o modificano la progressione del giocatore
  • Influenzano altri giocatori o lo stato condiviso del gioco
  • Eseguono azioni con requisiti di distanza, tempo o permesso (ad esempio, distanza dal negozio)

Validazione del tipo e della struttura

Un altro attacco che gli exploitatori potrebbero lanciare è inviare tipi tecnicamente validi ma renderli estremamente grandi, lunghi o altrimenti malformati. Ad esempio, se il server deve eseguire un'operazione costosa su una stringa che scala con la lunghezza, un exploitatori potrebbe inviare una stringa incredibilmente grande o malformata per rallentare il server.

Un altro attacco comune che gli exploitatori possono utilizzare implica l'invio di tables al posto di un Instance. Payload complessi possono mimare ciò che sarebbe un riferimento a un oggetto altrimenti ordinario.

Ad esempio, fornito con un sistema di negozio in-game dove i dati degli oggetti come i prezzi sono memorizzati in oggetti NumberValue, un exploitatori potrebbe eludere tutti gli altri controlli facendo quanto segue:

LocalScript in StarterPlayerScripts
local 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, -- I valori negativi potrebbero essere utilizzati, risultando nel dare valuta piuttosto che prenderla!
},
}
-- Invia payload malevolo al server (questo verrà rifiutato)
print(buyItemEvent:InvokeServer(payload)) -- Outputs "false Invalid item provided"
-- Invia un oggetto reale al server (questo passerà!)
print(buyItemEvent:InvokeServer(itemDatafolder["Real Blade"])) -- Outputs "true" e valuta rimanente se l'acquisto ha successo
Script in ServerScriptStorage
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local itemDataFolder = ReplicatedStorage:WaitForChild("ItemData")
local buyItemEvent = ReplicatedStorage:WaitForChild("BuyItemEvent")
local function buyItem(player, item)
-- Controlla se l'oggetto passato non è contraffatto e si trova nella cartella ItemData
if typeof(item) ~= "Instance" or not item:IsDescendantOf(itemDataFolder) then
return false, "Invalid item provided"
end
-- Il server può quindi procedere a elaborare l'acquisto in base al flusso di esempio qui sotto
end
-- Associa "buyItem()" al callback della funzione remota
buyItemEvent.OnServerInvoke = buyItem

Validazione dei valori

Oltre a convalidare i tipi e i dati, dovresti convalidare i valori passati attraverso RemoteEvents e RemoteFunctions, assicurandoti che siano validi e logici nel contesto richiesto. Questo è essenziale per i remoti che gestiscono valuta, oggetti, input numerici o contenuti generati dagli utenti. Devi assicurarti che i valori siano entro i limiti previsti, ad esempio una quantità di acquisto maggiore di zero o un ID oggetto corrispondente a un oggetto reale nel tuo gioco.

Nota che sia inf che NaN sono tipi di numero validi, ma entrambi possono causare problemi maggiori se un exploitatori li invia e non vengono gestiti correttamente. Usa math.isfinite() per rilevarli e rifiutarli:

-- Rifiuta valori numerici che sono NaN o +/-inf
if not math.isfinite(offeredGold) then
return false, "Invalid number provided"
end

Il pericolo di NaN

Un exploitatori può inviare NaN (Not a Number) come argomento. NaN è unicamente pericoloso perché è di tipo "number" ma fallisce tutti i confronti standard, permettendogli di eludere sottilmente controlli logici che sembrano sicuri. Vediamo un sistema di trading vulnerabile dove un giocatore fa un'offerta.

-- CODICE SERVER VULNERABILE
local function onCreateTradeOffer(player, offeredGold)
-- 1. CONTROLLI DI TIPO: Questo passa! typeof(NaN) è "number".
if typeof(offeredGold) ~= "number" then
return "Invalid offer"
end
-- 2. CONTROLLI DI INTERVALLO: Questo viene eluso!
-- (NaN < 0) è falso. (NaN > 1000000) è anche falso. Il controllo non fa nulla.
if offeredGold < 0 or offeredGold > 1000000 then
return "Offer out of range"
end
-- 3. CONTROLLI DELL'INVENTARIO: Questo viene eluso!
-- (NaN > player.Gold.Value) è falso.
if offeredGold > player.Gold.Value then
return "Not enough gold"
end
-- VULNERABILITÀ: Un'offerta di scambio fraudolenta con oro NaN viene creata!
createTrade(player, {gold = offeredGold})
return "Trade offer created."
end

L'exploitatori ha creato con successo un'offerta di scambio senza avere oro, perché ogni controllo è fallito silenziosamente. Inoltre, questo singolo valore NaN ora avvelenerà qualsiasi altro sistema che cerca di utilizzarlo e si diffonderà anche ad altri giocatori attraverso l'offerta.

Di seguito sono riportati alcuni esempi ad alto livello che coprono la progettazione sicura di funzionalità comuni in-game.

Negozio in-game

Considera un sistema di negozio in-game con un'interfaccia utente, ad esempio un menu di selezione prodotto con un pulsante "Acquista". Quando il pulsante viene premuto, puoi invocare una RemoteFunction tra il client e il server per richiedere l'acquisto. Tuttavia, è importante che il server, il gestore più affidabile del gioco, confermi che l'utente ha abbastanza soldi per acquistare l'oggetto.

Esempio di flusso di acquisto dal client al server tramite un RemoteEvent
Esempio di flusso di acquisto dal client al server tramite una RemoteFunction

Targeting delle armi

Gli scenari di combattimento richiedono particolare attenzione nella validazione dei valori, in particolare attraverso la mira e la validazione dei colpi.

Immagina un gioco in cui un giocatore può sparare un raggio laser a un altro giocatore. Invece che il client dica al server chi danneggiare, dovrebbe invece dire al server la posizione di origine del colpo e la parte/posizione che pensa di aver colpito. Il server può quindi convalidare quanto segue:

  • La posizione da cui il client riporta di aver sparato è vicina al personaggio del giocatore sul server. Nota che il server e il client differiranno leggermente a causa della latenza, quindi sarà necessario applicare una tolleranza extra.
  • La posizione che il client riporta di aver colpito è ragionevolmente vicina alla posizione della parte che il client riporta di aver colpito, sul server.
  • Non ci sono ostruzioni statiche tra la posizione da cui il client riporta di aver sparato e la posizione a cui il client riporta di aver sparato. Questo controllo garantisce che un client non stia tentando di sparare attraverso i muri. Nota che questo dovrebbe controllare solo la geometria statica per evitare che colpi validi vengano rifiutati a causa della latenza.

Inoltre, potresti voler implementare ulteriori validazioni lato server come segue:

  • Tieni traccia di quando il giocatore ha sparato l'arma per l'ultima volta e valida per assicurarti che non stia sparando troppo velocemente.
  • Tieni traccia della quantità di munizioni di ciascun giocatore sul server e conferma che un giocatore che spara abbia abbastanza munizioni per eseguire l'attacco dell'arma.
  • Se hai implementato squadre o un sistema di combattimento "giocatori contro bot", conferma che il personaggio colpito sia un nemico, non un compagno di squadra.
  • Conferma che il giocatore colpito sia vivo.
  • Memorizza lo stato dell'arma e del giocatore sul server e conferma che un giocatore che spara non sia bloccato da un'azione attuale come il ricaricamento o uno stato come la corsa.

Limitazione della frequenza

Ogni volta che la logica lato server può essere attivata dal client (tramite remoti, eventi Touched, prompt di prossimità, ClickDetectors, ecc.), c'è la possibilità che questa logica possa essere spammata da exploitatori o anche da utenti legittimi. La limitazione della frequenza controlla quanto frequentemente queste azioni possono essere eseguite, prevenendo abusi e sovraccarico del sistema. Sebbene sia pratico (e talvolta necessario) implementare limiti di frequenza sul client, non fare mai affidamento esclusivamente su un limite di frequenza lato client.

Per qualsiasi logica lato server che può essere attivata da un client, considera sempre il tasso massimo al quale tale logica dovrebbe essere eseguita. Molte azioni lato server devono avere la loro frequenza limitata per prevenire abusi o fallimenti, come:

  • Chiamate API di Roblox: Le API di backend come DataStoreService e BadgeService hanno limiti di frequenza integrati che causeranno il fallimento delle tue richieste se superati
  • Operazioni computazionalmente costose: Azioni che consumano risorse significative del server o impattano altri client
  • Esempio server: clonazione di modelli grandi da ServerStorage, anche se non vengono mai replicati ai client
  • Esempio client: istruire tutti i client connessi di aggiornare la loro GUI simultaneamente
  • Meccaniche sfruttabili: Azioni che potrebbero essere abusate se eseguite rapidamente, come concedere invulnerabilità, teletrasportare giocatori o assegnare valuta

Esempio di token bucket

Un approccio robusto e comune per la limitazione della frequenza è l'algoritmo token bucket. Immagina che ogni utente abbia un secchio che può contenere un certo numero di token. Per eseguire un'azione, l'utente deve spendere un token. Il secchio si riempie con nuovi token a un ritmo costante. Questo metodo consente brevi esplosioni di azioni quando necessario, ma previene spam sostenuto imponendo un tasso medio nel tempo.

--!strict
-- Modulo situato in 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
-- Crea un limitatore che consente al massimo `capacity` eventi, riempiendo di nuovo
-- completamente a un ritmo di `capacity / windowSeconds` token al secondo.
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
-- Restituisce false se l'utente supererebbe il proprio 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

Per utilizzare il modulo, prima crea una nuova istanza del limitatore con TokenBucket.new(capacity, windowSeconds). La capacity è il numero massimo di richieste che un utente può fare in un breve periodo, e windowSeconds determina quanto tempo ci vuole per riempire tutti quei token. Ad esempio, TokenBucket.new(5, 10) crea un limitatore che consente esplosioni di fino a 5 richieste e riempie un token ogni due secondi (10 secondi / 5 token).

Prima di eseguire logica protetta, fai una chiamata al metodo :allow(userId), negando l'azione se restituisce false. È anche buona pratica cancellare i dati del secchio dell'utente quando esce per prevenire perdite di memoria.

Il seguente esempio di script dimostra come proteggere un RemoteEvent utilizzato per un sistema di chat personalizzato dallo spam da parte dei giocatori.

-- Esempio di utilizzo in uno script server
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 messaggi ogni 10 secondi (capacità 5, riempie a 0.5 token/sec)
local chatLimiter = TokenBucket.new(5, 10)
ChatRemote.OnServerEvent:Connect(function(player: Player, message: string)
if not chatLimiter:allow(player.UserId) then
-- Troppo veloce: scarta la richiesta o invia un avviso di spam
return
end
-- Elabora il messaggio (dopo altre convalide)
broadcastMessage(player, message)
end)
-- Pulizia per prevenire perdite di memoria
Players.PlayerRemoving:Connect(function(p: Player)
chatLimiter.buckets[p.UserId] = nil
end)

Sicurezza delle istanze attivate dal client

ProximityPrompts, ClickDetectors e DragDetectors non sono remoti, ma un exploitatori può attivare i loro eventi da qualsiasi distanza, in qualsiasi momento, spesso ignorando proprietà come Enabled o MaxActivationDistance. Questi oggetti devono essere protetti con la stessa rigorosità dei remoti.

ProximityPrompt

  • Un exploitatori può attivare qualsiasi evento anche se Enabled è falso sul server.
  • Enabled, MaxActivationDistance e RequiresLineOfSight possono essere cambiati silenziosamente sul client, quindi qualsiasi ProximityPrompt replicato può essere visto e potenzialmente interagito da qualsiasi posizione.
  • Il server accetterà eventi di attesa (come PromptButtonHoldBegan) anche se il HoldDuration lato server è zero. Il client può manipolare il HoldDuration per eseguire interazioni in modo innaturale veloce.
  • Solo l'evento Triggered ha un controllo di distanza lato server. Altri eventi (come PromptButtonHoldBegan e TriggerEnded) non hanno controlli di distanza e possono essere inviati arbitrariamente da qualsiasi client.

ClickDetector

  • Non ci sono controlli sul server per alcun evento.
  • Gli exploitatori possono replicare qualsiasi evento a qualsiasi distanza, anche se il ClickDetector è disabilitato (MaxActivationDistance di 0 o DragDetector.Enabled falso).
  • Gli eventi possono essere replicati anche se il ClickDetector non è parentato a qualcosa di cliccabile.

DragDetector

  • Per eventi ereditati da ClickDetector, non ci sono controlli.
  • Per eventi di trascinamento, le proprietà Enabled e PermissionPolicy vengono controllate e rispettate dal server. Tutte le altre proprietà non vengono controllate.

Quando uno di questi eventi si attiva, il tuo script server dovrebbe eseguire la propria validazione prima di intraprendere qualsiasi azione:

  • Controlla se è abilitato: Assicurati che l'istanza sia destinata a essere abilitata controllando le sue proprietà (come Enabled, HoldDuration, RunLocally, ecc.) sul server. Questo passaggio non è necessario se l'oggetto è sempre abilitato.
  • Controlla lo stato del giocatore: Assicurati che il giocatore possa eseguire l'azione, dato lo stato attuale del giocatore. Il personaggio del giocatore è nel mondo? Il suo personaggio è abbastanza vicino all'oggetto? Il giocatore deve essere vivo per interagire con questo oggetto? Controlla qualsiasi altro stato del giocatore mantenuto dal tuo gioco sul server secondo necessità.
  • Applica limitazione della frequenza: Come trattato in Limitazione della frequenza, applica cooldown sul client e imposta limiti di frequenza sul server per prevenire che questi eventi vengano spamati e abusati. Per interazioni che ci si aspetta richiedano un tempo minimo, assicurati che i client non le completino troppo rapidamente.

Nota sulla proprietà di rete: Per impostazione predefinita, se uno di questi oggetti è figlio di parti non ancorate o di un'assemblaggio non ancorato, un exploitatori può prendere la proprietà di rete delle parti genitore e muoverle direttamente al proprio personaggio, eludendo i controlli di distanza. Per azioni critiche, utilizza parti ancorate o l'API di proprietà di rete insieme alla necessaria validazione lato server.

RemoteEvents e RemoteFunctions

Limita la portata e l'impatto di RemoteFunctions e RemoteEvents. Fai estrema attenzione nel caricare dinamicamente qualsiasi asset (anche texture o suoni) o nell'eseguire codice di gioco (soprattutto tramite require) basato sugli argomenti delle funzioni/eventi remoti. Evita di implementare remoti che consentano a un client di specificare un percorso arbitrario o un riferimento a un'istanza per il server da eliminare o modificare, anche se tali modifiche sembrano banali. I remoti che possono cambiare lo stato di un'istanza arbitraria o apportare modifiche diffuse all'albero DataModel possono spesso essere concatenati con altri bug per influenzare gravemente lo stato o la logica complessiva. Controlla non solo il tipo degli argomenti delle istanze, ma anche la classe e la posizione o struttura attesa all'interno del DataModel.

Comunicazione client a client

Alcuni remoti sono progettati per consentire a un client di attivare effetti su altri client. Questo accade quando un client attiva un remoto sul server, che poi utilizza RemoteEvent:FireAllClients(), RemoteEvent:FireClient() o metodi simili per trasmettere informazioni. Questo schema è pericoloso se non adeguatamente protetto: il server deve essere un custode, non solo un relay.

Immagina uno scenario in cui i giocatori possono lanciare un incantesimo di fulmine. Quando viene lanciato, i giocatori vicini vedono il lampo, sentono il suono e sperimentano un tremore della telecamera.

Uno script server vulnerabile potrebbe apparire così:

Codice server vulnerabile (script in ServerScriptStorage)
local castLightningEvent = game.ReplicatedStorage.CastLightning
-- Questo script server trasmette il messaggio a tutti senza validazione
castLightningEvent.OnServerEvent:Connect(function(player, strikePosition)
castLightningEvent:FireAllClients(strikePosition)
end)
Codice effetto client
local castLightningEvent = game.ReplicatedStorage.CastLightning
-- Crea effetti visivi e sonori sul client
local function createLightningEffect(strikePosition)
-- Codice per scuotere la telecamera
-- Codice per riprodurre un forte suono di fulmine
-- Codice per creare un fulmine visivo
print("Il fulmine colpisce a: " .. tostring(strikePosition))
end
-- Quando il server trasmette l'evento, questo client esegue l'effetto
castLightningEvent.OnClientEvent:Connect(createLightningEffect)

Un exploitatori può abusare di questo sistema vulnerabile:

  • Spam: Chiamando il remoto centinaia di volte al secondo, causando effetti continui che rendono il gioco ingiocabile
  • Dati non validi: Inviando nil, NaN o tipi errati che causano errori di script su tutti i client
  • Uso non autorizzato: Lanciando incantesimi che non hanno sbloccato o per cui non hanno risorse

Il server deve essere un custode, non un relay. Prima di trasmettere, deve convalidare la richiesta e applicare limitazioni di frequenza per giocatore. Ecco un esempio di alcuni metodi che potrebbero essere impiegati per convalidare la richiesta.

Codice server sicuro
local castLightningEvent = game.ReplicatedStorage.CastLightning
local playerCooldowns = {}
local COOLDOWN_TIME = 3
castLightningEvent.OnServerEvent:Connect(function(player, strikePosition)
-- 1. Validazione del tipo
if typeof(strikePosition) ~= "Vector3" then
return -- Tipo non valido, rifiuta silenziosamente
end
-- 2. Controlla per NaN (NaN ~= NaN è l'unico modo per rilevarlo)
if strikePosition.X ~= strikePosition.X then
return -- Contiene NaN, rifiuta
end
-- 3. Esempio di limitazione della frequenza
local lastCast = playerCooldowns[player] or 0
if tick() - lastCast < COOLDOWN_TIME then
return -- Ancora in cooldown
end
-- 4. Esempio di controllo dei permessi
if not player:GetAttribute("HasLightningSpell") then
return -- Il giocatore non ha questo incantesimo
end
-- 5. Esempio di validazione della distanza
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 -- Fuori portata
end
-- Tutti i controlli superati - sicuro da trasmettere
playerCooldowns[player] = tick()
castLightningEvent:FireAllClients(player, strikePosition)
end)

Implementando questi controlli, diventa più difficile per un exploitatori rovinare il gioco per altri giocatori. Il server funge da barriera protettiva, garantendo che solo azioni valide e autorizzate vengano trasmesse ad altri client.

Considerazioni speciali

Di seguito sono riportati alcuni casi aggiuntivi che dovresti considerare e che potrebbero richiedere una gestione specializzata.

Manipolazione del Data Store

Nei giochi che utilizzano DataStoreService per salvare i dati dei giocatori, gli exploitatori possono approfittare di dati non validi, condizioni di gara per corrompere i salvataggi o duplicare oggetti in un DataStore. Questo è particolarmente problematico nei giochi con scambi di oggetti, mercati e sistemi di valuta.

Assicurati che tutte le azioni eseguite tramite un RemoteEvent o RemoteFunction che influenzano i dati del giocatore con input del client siano sanificate in base a quanto segue:

  • I valori delle istanze non possono essere serializzati in un DataStore e falliranno. Utilizza la validazione dei tipi per prevenire questo.
  • I DataStore hanno limiti di dati. Le stringhe di lunghezza arbitraria devono essere controllate e/o limitate per evitare questo, insieme all'assicurarsi che chiavi arbitrarie illimitate non possano essere aggiunte a tabelle dal client.
  • Gli indici delle tabelle non possono essere NaN o nil. Itera su tutte le tabelle passate dal client e verifica che tutti gli indici siano validi.
  • I DataStore possono accettare solo caratteri UTF-8 validi, quindi dovresti sanificare tutte le stringhe fornite dal client tramite utf8.len() per assicurarti che siano valide. utf8.len() restituirà la lunghezza di una stringa, trattando i caratteri unicode come un singolo carattere; se viene incontrato un carattere UTF-8 non valido, restituirà nil e la posizione del carattere non valido. Nota che le stringhe UTF-8 non valide possono anche essere presenti nelle tabelle come chiavi e valori.
  • Sfruttamenti di numeri infiniti/NaN - Gli exploitatori possono inviare valori come -1/0, 0/0 o math.huge per causare valuta infinita o interrompere i calcoli. Valida sempre utilizzando i metodi descritti in il pericolo di NaN.

Le condizioni di gara possono verificarsi quando i giocatori manipolano il tempo tra le operazioni per duplicare oggetti o corrompere dati.

Esempio di sfruttamento del trading: Un giocatore avvia uno scambio, invia il proprio oggetto a un altro giocatore e poi esce immediatamente dal gioco. Se lo scambio si completa ma il salvataggio del loro DataStore fallisce a causa di dati non validi, si riuniscono con i loro oggetti originali mentre l'altro giocatore tiene gli oggetti scambiati - risultando in duplicazione.

Strategie di prevenzione:

  • Valida tutti i dati prima di qualsiasi operazione di scambio
  • Utilizza schemi simili a transazioni in cui i dati di tutti i giocatori vengono convalidati prima di impegnare eventuali modifiche
  • Implementa una corretta gestione degli errori che ripristina tutte le modifiche se una parte fallisce

MarketplaceService

Per interazioni che coinvolgono MarketplaceService, come pass o prodotti per sviluppatori, tutta la validazione degli acquisti e la concessione di oggetti devono avvenire sul server. In particolare, utilizza il callback ProcessReceipt per convalidare in modo sicuro le ricevute di acquisto. Non fidarti mai di un segnale lato client, come PromptProductPurchaseFinished, per confermare un acquisto senza verifica del server, poiché questi possono essere falsificati. Assicurati che la tua funzione ProcessReceipt controlli accuratamente i dettagli della ricevuta e conceda solo oggetti o valuta dopo aver confermato una transazione legittima con i server di Roblox, e preparati a gestire casi in cui un prodotto è già stato concesso per una data ricevuta.

© 2026 Roblox Corporation. Roblox, il logo Roblox e Powering Imagination sono tra i nostri marchi registrati e non registrati negli Stati Uniti. e altri paesi.