Ogni volta che un client può attivare un'azione sul server, c'è il potenziale per abusi. Mentre RemoteEvents e RemoteFunctions sono il vettore di attacco più comune, altre istanze come ProximityPrompt sono vulnerabili. Questa sezione descrive 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 livelli di validazione in base a ciò che fa il remoto.
Validazione del contesto/permissibilità
Questa è necessaria per i remoti che influenzano lo stato dell'esperienza, il progresso o altri giocatori. Il server deve verificare se il giocatore ha le autorizzazioni necessarie 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:
- Assegnano ricompense o modificano il progresso del giocatore
- Influenzano altri giocatori o lo stato condiviso dell'esperienza
- Eseguono azioni con requisiti di distanza, tempistica o autorizzazione (ad esempio, distanza dal negozio)
Validazione di tipo e 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 exploiter potrebbe inviare una stringa incredibilmente grande o malformata per appesantire il server.
Un altro attacco comune che gli exploitatori possono utilizzare comporta l'invio di tables al posto di un Instance. Payload complessi possono mimare quello che sarebbe un riferimento a un oggetto altrimenti ordinario.
Ad esempio, fornito con un sistema di negozio in esperienza dove i dati degli oggetti come i prezzi sono memorizzati in oggetti NumberValue, un exploiter potrebbe eludere tutti gli altri controlli facendo quanto segue:
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, -- Valori negativi potrebbero anche essere utilizzati, risultando nel dare valuta piuttosto che prenderla!},}-- Invia payload malevolo al server (questo verrà rifiutato)print(buyItemEvent:InvokeServer(payload)) -- Output "false Invalid item provided"-- Invia un oggetto reale al server (questo passerà!)print(buyItemEvent:InvokeServer(itemDatafolder["Real Blade"])) -- Output "true" e valuta residua se l'acquisto riesce
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 ad elaborare l'acquisto in base al flusso di esempio di seguito
end
-- Collega "buyItem()" al callback della funzione remota
buyItemEvent.OnServerInvoke = buyItem
Validazione dei valori
Oltre a convalidare tipi e 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 limiti attesi (ad esempio, una quantità di acquisto è maggiore di zero) e che gli ID o i nomi forniti siano validi (ad esempio, un itemId corrisponde a un oggetto reale nel tuo gioco).
Considerazioni speciali per i valori numerici: Sia inf che NaN sono tipi numerici validi, ma entrambi possono causare seri problemi se un exploit sono li invia e non vengono gestiti correttamente. Usa funzioni come questa per rilevarli e rifiutarli:
local function isNaN(n: number): boolean
-- NaN non è mai uguale a se stesso
return n ~= n
end
local function isInf(n: number): boolean
-- Il numero potrebbe essere -inf o +inf
return math.abs(n) == math.huge
end
Il pericolo di NaN
Un exploiter può inviare NaN (Not a Number) come argomento. NaN è unicamente pericoloso perché è di tipo "number" ma fallisce tutti i confronti standard, permettendo di eludere sottilmente controlli logici che sembrano sicuri. Diamo un'occhiata a 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 REIMENSIONE: 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 DI INVENTARIO: Questo viene eluso!
-- (NaN > player.Gold.Value) è falso.
if offeredGold > player.Gold.Value then
return "Not enough gold"
end
-- VULNERABILITÀ: Viene creata un'offerta di scambio fraudolenta con NaN oro!
createTrade(player, {gold = offeredGold})
return "Trade offer created."
end
L'exploiter ha creato con successo un'offerta di scambio senza avere oro, poiché ogni controllo è fallito silenziosamente. Inoltre, questo singolo valore NaN ora avvelenerà qualsiasi altro sistema che prova ad utilizzarlo, e si diffonderà persino ad altri giocatori attraverso l'offerta.
Di seguito sono riportati alcuni esempi generali che coprono la progettazione sicura delle comuni caratteristiche di gioco.
Negozio in esperienza
Considera un sistema di negozio in esperienza 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 dell'esperienza, confermi che l'utente ha abbastanza denaro per acquistare l'oggetto.

Targeting delle armi
Gli scenari di combattimento richiedono attenzione speciale alla validazione dei valori, in particolare attraverso la mira e la validazione dei colpi.
Immagina un'esperienza in cui un giocatore può sparare un raggio laser a un altro giocatore. Piuttosto che il client dire 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 vicino 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 che il client riporta di aver colpito. Questo controllo assicura che un client non stia cercando 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 convalide lato server come segue:
- Tieni traccia di quando il giocatore ha sparato per l'ultima volta e verifica 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 ha abbastanza munizioni per eseguire l'attacco con l'arma.
- Se hai implementato squadre o un sistema di combattimento "giocatori contro bot", conferma che il personaggio colpito è un nemico, non un compagno di squadra.
- Conferma che il giocatore colpito sia vivo.
- Memorizza lo stato delle armi e del giocatore sul server e conferma che un giocatore che spara non sia bloccato da un'azione corrente come ricaricare o uno stato come correre.
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 spammed da exploitatori o anche da utenti legittimi. La limitazione della frequenza controlla quanto frequentemente queste azioni possano 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 possa essere attivata da un client, considera sempre il limite 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 backend come DataStoreService e BadgeService hanno limiti di frequenza integrati che faranno fallire le tue richieste se superati
- Operazioni computazionalmente costose: Azioni che consumano risorse significative del server o influenzano altri client
- Esempio server: clonazione di grandi modelli da ServerStorage, anche se non vengono mai replicati ai client
- Esempio client: istruire tutti i client connessi di aggiornare il loro GUI simultaneamente
- Meccaniche sfruttabili: Azioni che potrebbero essere abusate se eseguite rapidamente, come concedere invulnerabilità, teleportare 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 una media di frequenza 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 limiter che consente al massimo `capacity` eventi, riempiendosi nuovamente a
-- una velocità 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 limiter con TokenBucket.new(capacity, windowSeconds). La capacity è il numero massimo di richieste che un utente può fare in un'esplosione rapida, e i windowSeconds determinano quanto tempo ci vuole per riempire tutti quei token. Ad esempio, TokenBucket.new(5, 10) crea un limiter che consente esplosioni di fino a 5 richieste e riempie un token ogni due secondi (10 secondi / 5 token).
Prima di eseguire logica protetta, effettua una chiamata al metodo :allow(userId), negando l'azione se restituisce false. È anche buona prassi cancellare i dati del secchio dell'utente quando esce per prevenire perdite di memoria.
Il seguente script di esempio dimostra come proteggere un RemoteEvent utilizzato per un sistema di chat personalizzato da essere spammed dai 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, si 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: abbandona 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 exploiter può attivare i loro eventi da qualsiasi distanza, in qualsiasi momento, ignorando spesso proprietà come Enabled o MaxActivationDistance. Questi oggetti devono essere protetti con la stessa rigore dei remoti.
ProximityPrompt
- Un exploiter 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 mantenimento (come PromptButtonHoldBegan) anche se il HoldDuration lato server è zero. Il client può manipolare il HoldDuration per eseguire interazioni in modo innaturalmente 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 la ClickDetector non è parent composable ad 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 viene attivato, il tuo script server deve 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, date le condizioni attuali 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 dalla tua esperienza sul server come necessario.
- Applica la limitazione della frequenza: come trattato in Limitazione della frequenza, applica cooldown sul client e applica limiti di frequenza sul server per prevenire che questi eventi siano spammed e abusati. Per interazioni che si prevede richiedano un tempo minimo, assicurati che i client non li 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 exploit possono assumere la proprietà di rete delle parti genitore e spostarle direttamente sul 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 eventuali asset (anche texture o suoni) o nell'eseguire il codice dell'esperienza (specialmente tramite require) basandoti sugli argomenti delle funzioni/eventi remoti. Evita di implementare remoti che consentono a un client di specificare un percorso arbitrario o un riferimento all'istanza per il server da cancellare o modificare, anche se tali modifiche sembrano banali. I remoti che possono cambiare lo stato di un'istanza arbitraria o apportare modifiche disseminate all'albero DataModel possono spesso essere concatenati con altri bug per influenzare gravemente lo stato complessivo o la logica. Controlla non solo il tipo degli argomenti per le istanze, ma anche la classe e la posizione o la struttura attesa all'interno del DataModel.
Comunicazione client a client
Alcuni remoti sono progettati per consentire a un client di attivare effetti su altri clienti. Questo avviene quando un client attiva un remoto sul server, che poi utilizza RemoteEvent:FireAllClients(), RemoteEvent:FireClient(), o metodi simili per trasmettere informazioni. Questo modello è pericoloso se non adeguatamente sicuro: il server deve essere un custode, non solo un relay.
Immagina uno scenario in cui i giocatori possono lanciare un incantesimo di fulmine. Quando lanciato, i giocatori nelle vicinanze vedono il lampo, sentono il suono e sperimentano scosse alla fotocamera.
Un script server vulnerabile potrebbe apparire come segue:
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 camera
-- 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 exploiter può abusare di questo sistema vulnerabile:
- Spam: Chiamando il remoto centinaia di volte al secondo, causando effetti continui che rendono l'esperienza 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 utilizzati 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 dei tipi
if typeof(strikePosition) ~= "Vector3" then
return -- Tipo non valido, rifiuta silenziosamente
end
-- 2. Controllo 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 trasmettere
playerCooldowns[player] = tick()
castLightningEvent:FireAllClients(player, strikePosition)
end)
Implementando questi controlli, diventa più difficile per un exploit rovinare l'esperienza per gli altri giocatori. Il server agisce come una barriera protettiva, garantendo che solo azioni valide e autorizzate vengano trasmesse agli altri client.
Considerazioni speciali
Le seguenti sono alcune ulteriori casi che dovresti considerare che possono richiedere una gestione specializzata.
Manipolazione del DataStore
Negli esperienze che utilizzano DataStoreService per salvare i dati dei giocatori, gli exploitatori possono approfittare di dati non validi, condizioni di race per corrompere i salvataggi o duplicare oggetti in un DataStore. Questo è particolarmente problematico nelle esperienze con trading 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 sanitize in base a quanto segue:
- 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 dovrebbero essere controllate e/o limitate per evitare ciò, insieme all'assicurarti che non possano essere aggiunti chiavi arbitrarie senza limiti alle tabelle da parte del client.
- Gli indici delle tabelle non possono essere NaN o nil. Itera su tutte le tabelle inviate dal client e verifica che tutti gli indici siano validi.
- I DataStore possono accettare solo caratteri UTF-8 validi, quindi dovresti sanitize tutte le stringhe fornite dal client tramite utf8.len() per assicurarti che siano valide. utf8.len() restituisce 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 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 rompere i calcoli. Valida sempre utilizzando i metodi descritti in il pericolo di NaN.
Le condizioni di race possono verificarsi quando i giocatori manipolano il tempo tra le operazioni per duplicare oggetti o corrompere dati.
Esempio di sfruttamento nei trading: Un giocatore avvia un trade, invia il proprio oggetto a un altro giocatore e poi lascia immediatamente il gioco. Se il trade si completa ma il loro salvataggio DataStore fallisce a causa di dati non validi, rientrano con i propri 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 trading
- Usa schemi simili a transazioni in cui i dati di tutti i giocatori sono validati prima di confermare qualsiasi modifica
- Implementa una gestione degli errori appropriata che ripristina tutte le modifiche se una parte fallisce
MarketplaceService
Per interazioni che coinvolgono MarketplaceService, come pass o prodotti sviluppatori, tutta la convalida degli acquisti e l'assegnazione degli oggetti devono avvenire sul server. In particolare, utilizza il callback ProcessReceipt per convalidare in modo sicuro i ricevute d'acquisto. Non fidarti mai di un segnale lato client, come PromptProductPurchaseFinished, per confermare un acquisto senza una verifica del server, poiché questi possono essere falsificati. Assicurati che la tua funzione ProcessReceipt controlli attentamente 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 ricevuta data.