Sempre que um cliente pode acionar uma ação no servidor, há potencial para abuso. Enquanto RemoteEvents e RemoteFunctions são os vetores de ataque mais comuns aqui, outras instâncias como ProximityPrompt também são suscetíveis. Esta seção aborda como proteger essa fronteira.
Validando entradas remotas
Cada pedaço de dado enviado de um cliente deve ser validado pelo servidor antes de ser usado. Aplique essas camadas de validação com base no que o remoto faz.
Validação de contexto/permissão
Isso é necessário para remotos que afetam o estado da experiência, progresso ou outros jogadores. O servidor deve verificar se o jogador possui as permissões necessárias para fazer a solicitação. Por exemplo, o jogador está próximo o suficiente de uma loja para comprar algo? Ele tem a chave necessária para abrir uma porta? O personagem dele está vivo?
A validação é necessária para remotos que:
- Concedem recompensas ou modificam o progresso do jogador
- Afetam outros jogadores ou o estado compartilhado da experiência
- Realizam ações com requisitos de distância, tempo ou permissão (por exemplo, distância da loja)
Validação de tipo e estrutura
Outro ataque que exploradores podem lançar é enviar tipos tecnicamente válidos, mas fazê-los extremamente grandes, longos ou de outra forma malformados. Por exemplo, se o servidor precisa realizar uma operação cara em uma string que escala com o comprimento, um explorador poderia enviar uma string incrivelmente grande ou malformada para sobrecarregar o servidor.
Outro ataque comum que exploradores podem usar envolve enviar tables no lugar de um Instance. Payloads complexos podem imitar o que seria uma referência de objeto comum.
Por exemplo, em um sistema de loja dentro da experiência onde dados de itens como preços são armazenados em objetos NumberValue, um explorador pode contornar todas as outras verificações fazendo o seguinte:
LocalScript em 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, -- Valores negativos também podem ser usados, resultando em dar moeda em vez de retirá-la!},}-- Enviar payload malicioso para o servidor (isso será rejeitado)print(buyItemEvent:InvokeServer(payload)) -- Saídas "false Item inválido fornecido"-- Enviar um item real para o servidor (isso será aceito!)print(buyItemEvent:InvokeServer(itemDataFolder["Real Blade"])) -- Saídas "true" e moeda restante se a compra for bem-sucedida
Script em ServerScriptStorage
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local itemDataFolder = ReplicatedStorage:WaitForChild("ItemData")
local buyItemEvent = ReplicatedStorage:WaitForChild("BuyItemEvent")
local function buyItem(player, item)
-- Verifique se o item passado não está falsificado e está na pasta ItemData
if typeof(item) ~= "Instance" or not item:IsDescendantOf(itemDataFolder) then
return false, "Item inválido fornecido"
end
-- O servidor pode então processar a compra com base no fluxo de exemplo abaixo
end
-- Vincular "buyItem()" ao callback da função remota
buyItemEvent.OnServerInvoke = buyItem
Validação de valores
Além de validar tipos e dados, você deve validar os valores passados através de RemoteEvents e RemoteFunctions, assegurando que sejam válidos e lógicos no contexto solicitado. Isso é essencial para remotos que lidam com moeda, itens, entradas numéricas ou conteúdo gerado pelo usuário. Você deve garantir que os valores estejam dentro dos limites esperados (por exemplo, uma quantidade de compra maior que zero) e que os IDs ou nomes fornecidos sejam válidos (por exemplo, um itemId corresponde a um item real em seu jogo).
Considerações especiais para valores numéricos: Tanto inf quanto NaN são tipos numéricos válidos, mas ambos podem causar problemas sérios se um explorador os enviar e eles não forem tratados corretamente. Use funções como estas para detectá-los e rejeitá-los:
local function isNaN(n: number): boolean
-- NaN nunca é igual a si mesmo
return n ~= n
end
local function isInf(n: number): boolean
-- O número pode ser -inf ou +inf
return math.abs(n) == math.huge
end
O perigo do NaN
Um explorador pode enviar NaN (Not a Number) como argumento. NaN é particularmente perigoso porque é do tipo "número" mas falha em todas as comparações padrão, permitindo que ele contorne sutilmente verificações lógicas que parecem seguras. Vamos observar um sistema de troca vulnerável onde um jogador faz uma oferta.
-- CÓDIGO DO SERVIDOR VULNERÁVEL
local function onCreateTradeOffer(player, offeredGold)
-- 1. VERIFICAÇÃO DE TIPO: Isso passa! typeof(NaN) é "number".
if typeof(offeredGold) ~= "number" then
return "Oferta inválida"
end
-- 2. VERIFICAÇÃO DE FAIXA: Isso é contornado!
-- (NaN < 0) é falso. (NaN > 1000000) também é falso. A verificação não faz nada.
if offeredGold < 0 or offeredGold > 1000000 then
return "Oferta fora do intervalo"
end
-- 3. VERIFICAÇÃO DE INVENTÁRIO: Isso é contornado!
-- (NaN > player.Gold.Value) é falso.
if offeredGold > player.Gold.Value then
return "Ouro insuficiente"
end
-- VULNERABILIDADE: Uma oferta de troca fraudulenta com ouro NaN é criada!
createTrade(player, {gold = offeredGold})
return "Oferta de troca criada."
end
O explorador conseguiu criar uma oferta de troca sem ter nenhum ouro, porque todas as verificações falharam silenciosamente. Além disso, esse único valor NaN agora contaminaria qualquer outro sistema que tentasse usá-lo, e se espalharia para outros jogadores através da oferta.
Abaixo estão alguns exemplos de alto nível que cobrem o design seguro de recursos comuns em jogos.
Loja dentro da experiência
Considere um sistema de loja dentro da experiência com uma interface de usuário, por exemplo, um menu de seleção de produtos com um botão "Comprar". Quando o botão é pressionado, você pode invocar uma RemoteFunction entre o cliente e o servidor para solicitar a compra. No entanto, é importante que o servidor, o gerente mais confiável da experiência, confirme que o usuário tem dinheiro suficiente para comprar o item.

Mira de armas
Cenários de combate exigem atenção especial à validação de valores, particularmente por meio da mira e validação de acertos.
Imagine uma experiência onde um jogador pode disparar um raio laser contra outro jogador. Em vez de o cliente informar ao servidor quem deve ser danificado, ele deve informar ao servidor a posição de origem do disparo e a parte/posição que pensa ter atingido. O servidor pode então validar o seguinte:
- A posição de onde o cliente relata ter disparado está perto do personagem do jogador no servidor. Observe que o servidor e o cliente diferirão ligeiramente devido à latência, então tolerância extra precisará ser aplicada.
- A posição que o cliente relata ter atingido está razoavelmente próxima da posição da parte que o cliente relata ter atingido, no servidor.
- Não há obstruções estáticas entre a posição de onde o cliente relata ter disparado e a posição onde o cliente relata ter atingido. Essa verificação garante que um cliente não está tentando disparar através de paredes. Observe que isso deve apenas verificar a geometria estática para evitar que disparos válidos sejam rejeitados devido à latência.
Além disso, você pode querer implementar mais validações do lado do servidor, como:
- Rastrear quando o jogador disparou sua arma pela última vez e validar para garantir que não esteja disparando muito rápido.
- Rastrear a quantidade de munição de cada jogador no servidor e confirmar que um jogador disparador tenha munição suficiente para executar o ataque com a arma.
- Se você implementou equipes ou um sistema de combate "jogadores contra bots", confirme que o personagem atingido é um inimigo, não um companheiro de equipe.
- Confirme que o jogador atingido está vivo.
- Armazene o estado da arma e do jogador no servidor e confirme que um jogador disparador não está bloqueado por uma ação atual, como recarregar ou um estado como correr.
Limitação de taxa
Sempre que a lógica do lado do servidor puder ser acionada pelo cliente (via remotos, eventos Touched, prompts de proximidade, ClickDetectors, etc.), existe a possibilidade de que essa lógica possa ser sobrecarregada por exploradores ou até mesmo usuários legítimos. A limitação de taxa controla com que frequência essas ações podem ser realizadas, evitando abusos e sobrecargas no sistema. Embora seja prático (e às vezes necessário) implementar limites de taxa no cliente, nunca confie apenas em um limite de taxa do lado do cliente.
Para qualquer lógica do lado do servidor que pode ser acionada por um cliente, sempre considere a taxa máxima na qual tal lógica deve ser executada. Muitas ações do lado do servidor devem ter sua frequência limitada para evitar abusos ou falhas, tais como:
- Chamadas de API do Roblox: APIs de backend, como DataStoreService e BadgeService, têm limites de taxa embutidos que farão suas solicitações falharem se excedidos.
- Operações computacionalmente caras: Ações que consomem recursos significativos do servidor ou impactam outros clientes.
- Exemplo do servidor: clonagem de grandes modelos do ServerStorage, mesmo que nunca sejam replicados para os clientes.
- Exemplo do cliente: instruindo todos os clientes conectados a atualizar suas GUI simultaneamente.
- Mecânicas exploráveis: Ações que poderiam ser abusadas se realizadas rapidamente, como conceder invulnerabilidade, teleportar jogadores ou conceder moeda.
Exemplo do balde de tokens
Uma abordagem robusta e comum para limitação de taxa é o algoritmo do balde de tokens. Imagine que cada usuário tem um balde que pode conter um certo número de tokens. Para realizar uma ação, o usuário deve gastar um token. O balde se enche novamente com novos tokens a uma taxa constante. Esse método permite curtos períodos de ações quando necessário, mas evita o spam contínuo, aplicando uma média de taxa ao longo do tempo.
--!strict
-- Módulo localizado em 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
-- Cria um limitador que permite no máximo `capacity` eventos, reabastecendo
-- de volta ao total a uma taxa de `capacity / windowSeconds` tokens por segundo.
function TokenBucket.new(capacity: number, windowSeconds: number): TokenBucketT
assert(capacity >= 1, "capacity deve ser >= 1")
assert(windowSeconds > 0, "windowSeconds deve ser > 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
-- Retorna false se o usuário excederia seu 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
Para usar o módulo, você primeiro cria uma nova instância do limitador com TokenBucket.new(capacity, windowSeconds). A capacity é o número máximo de solicitações que um usuário pode fazer em um curto período, e a windowSeconds determina quanto tempo leva para reabastecer todos esses tokens. Por exemplo, TokenBucket.new(5, 10) cria um limitador que permite explosões de até 5 solicitações e reabastece um token a cada dois segundos (10 segundos / 5 tokens).
Antes de executar a lógica protegida, faça uma chamada ao método :allow(userId), negando a ação se retornar false. Também é uma boa prática limpar os dados do balde do usuário quando ele sair para evitar vazamentos de memória.
O seguinte script de exemplo demonstra como proteger um RemoteEvent usado para um sistema de chat personalizado contra ser sobrecarregado por jogadores.
-- Exemplo de uso em um script de servidor
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 mensagens a cada 10 segundos (capacidade 5, reabastece a 0.5 tokens/sec)
local chatLimiter = TokenBucket.new(5, 10)
ChatRemote.OnServerEvent:Connect(function(player: Player, message: string)
if not chatLimiter:allow(player.UserId) then
-- Muito rápido: descartar a solicitação ou enviar um aviso de spam
return
end
-- Processar mensagem (após outras validações)
broadcastMessage(player, message)
end)
-- Limpeza para evitar vazamentos de memória
Players.PlayerRemoving:Connect(function(p: Player)
chatLimiter.buckets[p.UserId] = nil
end)
Protegendo instâncias acionadas pelo cliente
ProximityPrompts, ClickDetectors e DragDetectors não são remotos, mas um explorador pode acionar seus eventos de qualquer distância, a qualquer momento, muitas vezes ignorando propriedades como Enabled ou MaxActivationDistance. Esses objetos devem ser protegidos com o mesmo rigor que remotos.
ProximityPrompt
- Um explorador pode acionar qualquer evento mesmo se Enabled estiver falso no servidor.
- Enabled, MaxActivationDistance e RequiresLineOfSight podem ser alterados silenciosamente no cliente, então qualquer ProximityPrompt replicado pode ser visto e potencialmente interagido de qualquer lugar.
- O servidor aceitará eventos de hold (como PromptButtonHoldBegan) mesmo se o HoldDuration do lado do servidor for zero. O cliente pode manipular o HoldDuration para realizar interações de maneira anormalmente rápida.
- Apenas o evento Triggered tem uma verificação de distância do lado do servidor. Outros eventos (como PromptButtonHoldBegan e TriggerEnded) não têm verificação de distância e podem ser enviados arbitrariamente por qualquer cliente.
ClickDetector
- Não há verificações no servidor para quaisquer eventos.
- Exploradores podem replicar qualquer evento a qualquer distância, mesmo se o ClickDetector estiver desativado (com MaxActivationDistance igual a 0 ou DragDetector.Enabled falso).
- Eventos podem ser replicados mesmo se o ClickDetector não estiver parentado a algo clicável.
DragDetector
- Para eventos herdados do ClickDetector, não há verificações.
- Para eventos de arrastar, as propriedades Enabled e PermissionPolicy são verificadas e respeitadas pelo servidor. Todas as outras propriedades não são verificadas.
Quando um desses eventos é acionado, seu script do servidor deve realizar sua própria validação antes de tomar qualquer ação:
- Verifique se está habilitado: Certifique-se de que a instância está destinada a estar habilitada verificando suas propriedades (como Enabled, HoldDuration, RunLocally, etc.) no servidor. Essa etapa não é necessária se o objeto estiver sempre habilitado.
- Verifique o estado do jogador: Certifique-se de que o jogador pode realizar a ação, dada a situação atual do jogador. O personagem do jogador está no mundo? O personagem está próximo o suficiente do objeto? O jogador deve estar vivo para interagir com esse objeto? Verifique qualquer outro estado do jogador mantido pela sua experiência no servidor conforme necessário.
- Aplique limitação de taxa: Como abordado em Limitação de taxa, aplique cooldowns no cliente e imponha limites de taxa no servidor para evitar que esses eventos sejam sobrecarregados e abusados. Para interações que devem levar um tempo mínimo, certifique-se de que os clientes não estão completando-as muito rapidamente.
Nota sobre a propriedade de rede: Por padrão, se qualquer um desses objetos for filho de partes não ancoradas ou de um conjunto não ancorado, um explorador pode assumir a propriedade de rede das partes pai e movê-las diretamente para seu personagem, contornando as verificações de distância. Para ações críticas, use partes ancoradas ou a API de propriedade de rede junto com a validação necessária do lado do servidor.
RemoteEvents e RemoteFunctions
Limite o escopo e o impacto de RemoteFunctions e RemoteEvents. Tenha extremo cuidado ao carregar dinamicamente quaisquer ativos (mesmo texturas ou sons) ou executar código da experiência (especialmente via require) com base nos argumentos de funções/e eventos remotos. Evite implementar remotos que permitam a um cliente especificar um caminho ou referência de instância arbitrários para o servidor excluir ou modificar, mesmo que tais modificações pareçam triviais. Remotos que podem alterar o estado de instância arbitrária ou fazer alterações generalizadas na árvore DataModel podem frequentemente ser encadeados com outros bugs para impactar severamente o estado ou a lógica geral. Verifique não apenas o tipo dos argumentos da instância, mas também a classe e a localização ou estrutura esperada dentro do DataModel.
Comunicação cliente a cliente
Alguns remotos são projetados para permitir que um cliente desencadeie efeitos em outros clientes. Isso acontece quando um cliente dispara um remoto para o servidor, que então usa RemoteEvent:FireAllClients(), RemoteEvent:FireClient() ou métodos similares para relatar informações. Esse padrão é perigoso se não estiver adequadamente seguro - o servidor deve ser um guardião, e não apenas um retransmissor.
Imagine um cenário onde os jogadores podem lançar um feitiço de raio. Quando lançado, os jogadores próximos veem o flash, ouvem o som e experienciam tremores na câmera.
Um script de servidor vulnerável poderia ser assim:
Código de servidor vulnerável (script em ServerScriptStorage)
local castLightningEvent = game.ReplicatedStorage.CastLightning
-- Este script do servidor retransmite a mensagem para todos sem validação
castLightningEvent.OnServerEvent:Connect(function(player, strikePosition)
castLightningEvent:FireAllClients(strikePosition)
end)
Código de efeito do cliente
local castLightningEvent = game.ReplicatedStorage.CastLightning
-- Cria efeitos visuais e sonoros no cliente
local function createLightningEffect(strikePosition)
-- Código para chacoalhar a câmera
-- Código para tocar um alto som de raio
-- Código para criar um raio visual
print("Raio atinge em: " .. tostring(strikePosition))
end
-- Quando o servidor transmite o evento, este cliente executa o efeito
castLightningEvent.OnClientEvent:Connect(createLightningEffect)
Um explorador pode abusar desse sistema vulnerável:
- Spam: Chamando o remoto centenas de vezes por segundo, causando efeitos contínuos que tornam a experiência injogável.
- Dados inválidos: Enviando nil, NaN ou tipos errados que causam erros de script em todos os clientes.
- Uso não autorizado: Lançando feitiços que não desbloquearam ou que não têm recursos suficientes.
O servidor deve ser um guardião, não uma retransmissão. Antes de retransmitir, ele deve validar a solicitação e aplicar limitação de taxa por jogador. Aqui está um exemplo de alguns métodos que poderiam ser empregados para validar a solicitação.
Código de servidor seguro
local castLightningEvent = game.ReplicatedStorage.CastLightning
local playerCooldowns = {}
local COOLDOWN_TIME = 3
castLightningEvent.OnServerEvent:Connect(function(player, strikePosition)
-- 1. Validação de tipo
if typeof(strikePosition) ~= "Vector3" then
return -- Tipo inválido, rejeitar silenciosamente
end
-- 2. Verifique se há NaN (NaN ~= NaN é a única maneira de detectá-lo)
if strikePosition.X ~= strikePosition.X then
return -- Contém NaN, rejeitar
end
-- 3. Exemplo de limitação de taxa
local lastCast = playerCooldowns[player] or 0
if tick() - lastCast < COOLDOWN_TIME then
return -- Ainda em espera
end
-- 4. Exemplo de verificação de permissão
if not player:GetAttribute("HasLightningSpell") then
return -- O jogador não tem esse feitiço
end
-- 5. Exemplo de validação de distância
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 -- Fora de alcance
end
-- Todas as verificações passaram - seguro para retransmitir
playerCooldowns[player] = tick()
castLightningEvent:FireAllClients(player, strikePosition)
end)
Ao implementar essas verificações, se torna mais difícil para um explorador arruinar a experiência para outros jogadores. O servidor atua como uma barreira protetora, garantindo que apenas ações válidas e autorizadas sejam retransmitidas para outros clientes.
Considerações especiais
As seguintes são alguns casos adicionais que você deve considerar e que podem exigir um tratamento especializado.
Manipulação de armazenamento de dados
Em experiências que usam o DataStoreService para salvar dados de jogadores, exploradores podem se aproveitar de dados inválidos, condições de corrida para corromper salvamentos ou duplicar itens em um DataStore. Isso é especialmente problemático em experiências com trocas de itens, mercados e sistemas de moeda.
Garanta que quaisquer ações realizadas através de um RemoteEvent ou RemoteFunction que afetam dados do jogador com entrada do cliente sejam sanitizadas com base no seguinte:
- Valores de instância não podem ser serializados em um DataStore e falharão. Utilize validação de tipo para evitar isso.
- Os DataStores têm limites de dados. Strings de comprimento arbitrário devem ser verificadas e/ou limitadas para evitar isso, além de garantir que chaves arbitrárias ilimitadas não possam ser adicionadas a tabelas pelo cliente.
- Os índices de tabelas não podem ser NaN ou nil. Percorra todas as tabelas passadas pelo cliente e verifique se todos os índices são válidos.
- Os DataStores só podem aceitar caracteres UTF-8 válidos, portanto, você deve sanitizar todas as strings fornecidas pelo cliente via utf8.len() para garantir que sejam válidas. utf8.len() retornará o comprimento de uma string, tratando caracteres unicode como um único caractere; se um caractere UTF-8 inválido for encontrado, retornará nil e a posição do caractere inválido. Observe que strings UTF-8 inválidas também podem estar presentes em tabelas como chaves e valores.
- Exploits de número infinito/NaN - Exploradores podem enviar valores como -1/0, 0/0 ou math.huge para causar moeda infinita ou quebrar cálculos. Sempre valide usando os métodos descritos em o perigo do NaN.
Condições de corrida podem ocorrer quando jogadores manipulam o tempo entre operações para duplicar itens ou corromper dados.
Exemplo de exploit de troca: Um jogador inicia uma troca, envia seu item para outro jogador e imediatamente sai do jogo. Se a troca for concluída, mas o salvamento do DataStore falhar devido a dados inválidos, ele se reconecta com seus itens originais enquanto o outro jogador mantém os itens trocados - resultando em duplicação.
Estratégias de prevenção:
- Valide todos os dados antes de quaisquer operações de troca.
- Utilize padrões semelhantes a transações onde os dados de todos os jogadores são validados antes de fazer quaisquer alterações.
- Implemente tratamento de erros adequado que reverta todas as alterações se qualquer parte falhar.
MarketplaceService
Para interações envolvendo MarketplaceService, como passes ou produtos do desenvolvedor, todas as validações de compra e concessão de itens devem ocorrer no servidor. Especificamente, utilize o callback ProcessReceipt para validar compras de forma segura. Nunca confie em um sinal do lado do cliente, como PromptProductPurchaseFinished, para confirmar uma compra sem verificação do servidor, pois esses podem ser falsificados. Certifique-se de que sua função ProcessReceipt verifica minuciosamente os detalhes do recibo e só concede itens ou moeda após confirmar uma transação legítima com os servidores do Roblox, e esteja preparado para lidar com casos onde um produto já foi concedido para um recibo dado.