Validar o que vem do cliente: o NaN que passa por todas as suas checagens
Todo dado que o cliente manda precisa ser validado no servidor. Este guia percorre as quatro camadas que a documentação oficial descreve, com o caso do NaN, que é do tipo number e mesmo assim escapa de qualquer comparação.
Produzido com IA a partir das fontes citadas abaixo, e conferido por checagens automáticas contra essas mesmas fontes. Nenhuma pessoa leu este texto antes da publicação. Política Editorial

A frase que sustenta toda a segurança de um jogo Roblox cabe numa linha, e a documentação oficial a escreve sem rodeio: todo dado enviado pelo cliente precisa ser validado pelo servidor antes de ser usado.
O problema é que "validar" soa como uma coisa só, e são quatro — em camadas, com ordem entre elas. Pular uma delas não produz erro; produz um jogo que funciona perfeitamente até alguém descobrir o buraco. Este guia percorre as quatro, e termina no caso que derruba as defesas mais bem-intencionadas: o NaN.
O problema
A porta existe por desenho. Toda vez que o cliente pode disparar uma ação no servidor existe potencial de abuso — não é falha de implementação, é a natureza de ter cliente e servidor.
E a porta é mais larga do que a maioria imagina. RemoteEvent e RemoteFunction são o vetor mais comum, mas a documentação lembra que outras instâncias, como ProximityPrompt, também são suscetíveis. Quem blindou só os remotes blindou a parte que estava olhando.
O desenho que mais aparece em jogo pequeno é o mesmo em todo lugar: uma pasta na ReplicatedStorage com os itens da loja, cada um com um NumberValue de preço, e um remote que recebe "o item que o jogador quer comprar". É conveniente, é legível — e é exatamente o formato que o exemplo oficial usa para mostrar o ataque.
Vale nomear o engano que sustenta tudo isso, porque ele não é burrice: é economia. O cliente já sabe se o jogador tem dinheiro, se está perto da loja, se o item existe. Escrever a checagem lá é natural, o botão fica desabilitado, o jogo se comporta certo, e a tela nunca mostra um estado inválido. O problema é que nada disso é uma trava — é uma conveniência de interface. O código do cliente roda na máquina de quem joga, e quem joga pode trocá-lo por outro. Toda regra que existe só no cliente é uma regra que o jogo pede com educação.
O teste mental que resolve a dúvida caso a caso é sempre o mesmo: se o cliente mandasse qualquer coisa por este remote, o que aconteceria? Se a resposta for alguma coisa diferente de "nada", falta validação. E "o cliente nunca mandaria isso" não é resposta — o cliente que importa é o que foi reescrito para mandar.
Passo a passo
Camada 1 — contexto e permissão. Antes de olhar o dado, olhe quem está pedindo. O servidor precisa verificar se o jogador tem permissão para fazer aquele pedido: ele está perto o bastante da loja para comprar? Tem a chave que abre a porta? O personagem está vivo?
A documentação lista três situações em que essa camada é obrigatória: remote que concede recompensa ou mexe em progressão, remote que afeta outros jogadores ou estado compartilhado, e remote com exigência de distância, tempo ou permissão. Se o seu remote cai em qualquer uma dessas, a checagem de contexto não é opcional.
Esta é a camada que mais se esquece, porque ela não tem cara de segurança — tem cara de regra de jogo. O cliente já sabe que o jogador está longe da loja, então o botão nem aparece. O servidor não sabe de nada disso.
Camada 2 — tipo e estrutura. Aqui entram dois ataques diferentes.
O primeiro é o volume: mandar tipos tecnicamente válidos, mas gigantes, longos ou malformados. Se o servidor faz uma operação cujo custo cresce com o tamanho da string, uma string enorme ou malformada serve para atolar o servidor. O tipo está certo; o tamanho é a arma.
O segundo é mais elegante e mais caro: mandar uma tabela no lugar de uma Instance. Payload complexo consegue imitar o que seria uma referência de objeto comum — uma tabela com Name, ClassName, Parent e um Price aninhado com Value zerado tem a mesma forma de leitura que o objeto real. Se o seu código só lê campos, ele não percebe a diferença.
E o exemplo oficial faz questão de anotar o detalhe que transforma o roubo em prejuízo: valor negativo no preço pode virar entrega de moeda em vez de cobrança. Não é só comprar de graça — é o jogo pagando o comprador.
A defesa é curta e é sempre a mesma: typeof(item) ~= "Instance" derruba a tabela, e item:IsDescendantOf(pastaDeItens) derruba a Instance verdadeira que veio de outro lugar. As duas juntas, nunca uma só.
Camada 3 — valor. Além do tipo, é preciso validar os valores que passam por RemoteEvent e RemoteFunction, garantindo que façam sentido no contexto pedido. A documentação é clara sobre onde isso é essencial: remote que lida com moeda, item, número ou conteúdo do usuário.
Os exemplos são do tipo que parece óbvio depois de escrito: a quantidade comprada precisa ser maior que zero, e o id pedido precisa corresponder a um item real do jogo.
Camada 4 — taxa. Não adianta cada chamada ser válida se cabem centenas por segundo. Essa camada não substitui as outras; ela existe porque validação cara chamada em repetição vira o próprio ataque.
E ela pega uma família de problemas que nenhuma das outras três pega. Um remote de "abrir baú" pode estar perfeitamente validado — jogador vivo, perto do baú, baú existe — e ainda assim ser chamado sessenta vezes no mesmo quadro, esvaziando a recompensa de um dia numa fração de segundo. Cada chamada, sozinha, é legítima. O abuso está no ritmo.
O intervalo certo não é um número universal: ele sai do jogo. A pergunta é quanto tempo a ação leva para um humano executar de verdade, e o limite fica um pouco abaixo disso. Apertado demais, jogador com conexão instável é punido por algo que não fez. Frouxo demais, não protege nada.
Artefato
O módulo abaixo junta as quatro camadas num lugar só. Ele não decide nada de jogo: devolve (ok, motivo) e deixa o handler do remote desistir no primeiro false.
A ordem dentro de Validacao.numero não é estética, e é o ponto do artigo inteiro: a checagem de finitude vem antes da checagem de faixa. A próxima seção explica por quê.
Erro comum
O erro comum não é esquecer de validar. É validar e achar que acabou.
A documentação avisa que tanto inf quanto NaN são tipos numéricos válidos. O NaN é perigoso justamente por causa do tipo dele: para o Luau, NaN é um número como qualquer outro. E ele falha em todas as comparações padrão, o que permite atravessar checagens que parecem seguras.
O exemplo oficial usa um sistema de troca. O servidor recebe a quantidade de ouro oferecida e faz duas checagens que qualquer revisor aprovaria: confere o tipo, confere a faixa. Os comentários do próprio código da documentação contam o que acontece — na checagem de tipo, isso passa, porque o tipo de NaN é número; na checagem de faixa, isso é contornado, porque nenhuma comparação com NaN devolve verdadeiro. Nem menor que zero, nem maior que o teto. As duas dão falso, o código segue em frente, e o servidor aceita uma oferta que não é um número.
O estrago depois disso é livre: NaN contamina qualquer conta que toque nele, e uma vez gravado num DataStore vira saldo permanente que nenhuma comparação consegue corrigir — porque toda comparação com ele continua dando falso.
A correção cabe numa linha, e é a que a documentação recomenda: use math.isfinite() para detectar e recusar esses valores. O detalhe é onde a linha entra. Colocada depois da checagem de faixa, ela não protege nada, porque o código já passou. Ela é a primeira coisa depois da checagem de tipo, sempre.
Próximo passo
Com as quatro camadas no lugar, o passo seguinte é parar de confiar na memória: toda vez que a validação mora dentro do handler, ela é copiada, adaptada e — no terceiro remote — esquecida.
Centralizar como no módulo acima muda a pergunta de revisão. Em vez de "este handler valida?", que exige ler o corpo inteiro de cada um, a pergunta vira "este handler chama Validacao antes de mexer em estado?", que se responde olhando as primeiras linhas.
O exercício que vale fazer hoje, e que costuma render mais do que qualquer refatoração: liste todos os OnServerEvent e OnServerInvoke do seu jogo num lugar só. Para cada um, escreva ao lado qual das quatro camadas ele tem. A lista quase sempre é mais curta do que se imagina — e os buracos aparecem sozinhos, sem precisar ler código.
Vale lembrar que ProximityPrompt entra nessa lista. Ele não parece um remote, mas a documentação o cita ao lado deles justamente porque é uma ação que o cliente dispara no servidor. Prompt com MaxActivationDistance generoso e nenhuma checagem de distância no servidor é o mesmo buraco com outra roupa.
Depois disso, vale percorrer os outros tópicos de segurança que a própria documentação indica na barra lateral — a fronteira cliente-servidor é a porta mais movimentada, mas não é a única.
Fontes
Artefato pronto pra usar
-- Validacao.lua — um lugar só para conferir o que chega do cliente.
--
-- As quatro camadas da documentação, na ordem em que vale rodar:
-- 1. contexto/permissão (este jogador PODE pedir isso?)
-- 2. tipo e estrutura (é mesmo Instance? é mesmo string curta?)
-- 3. valor (está dentro da faixa? é finito?)
-- 4. taxa (quantas vezes por segundo?)
--
-- Cada função devolve (ok, motivo). O handler do remote nunca decide
-- sozinho: ele pergunta aqui e desiste no primeiro "false".
local Validacao = {}
local TAMANHO_MAXIMO_DE_TEXTO = 200
-- ── camada 2: a tabela disfarçada de Instance ────────────────────────
-- typeof() de uma tabela devolve "table", nunca "Instance". E mesmo uma
-- Instance de verdade precisa estar DENTRO da pasta que você controla.
function Validacao.instanciaDe(valor, pasta)
if typeof(valor) ~= "Instance" then
return false, "nao e Instance"
end
if not valor:IsDescendantOf(pasta) then
return false, "Instance fora da pasta permitida"
end
return true
end
-- ── camada 2: texto ─────────────────────────────────────────────────
function Validacao.texto(valor)
if typeof(valor) ~= "string" then
return false, "nao e string"
end
if #valor == 0 or #valor > TAMANHO_MAXIMO_DE_TEXTO then
return false, "tamanho de string fora do aceito"
end
return true
end
-- ── camada 3: número ────────────────────────────────────────────────
-- A ORDEM AQUI NÃO É ESTÉTICA. math.isfinite() vem ANTES da faixa,
-- porque NaN passa reto por qualquer comparação de menor/maior.
function Validacao.numero(valor, minimo, maximo)
if typeof(valor) ~= "number" then
return false, "nao e number"
end
if not math.isfinite(valor) then
return false, "numero nao finito (NaN ou inf)"
end
if valor < minimo or valor > maximo then
return false, "numero fora da faixa"
end
return true
end
function Validacao.inteiroPositivo(valor, maximo)
local ok, motivo = Validacao.numero(valor, 1, maximo)
if not ok then
return false, motivo
end
if valor % 1 ~= 0 then
return false, "esperava inteiro"
end
return true
end
-- ── camada 1: contexto ──────────────────────────────────────────────
function Validacao.vivoEPerto(player, alvo, distanciaMaxima)
local character = player.Character
local humanoid = character and character:FindFirstChildOfClass("Humanoid")
if not humanoid or humanoid.Health <= 0 then
return false, "personagem morto ou ausente"
end
local raiz = character:FindFirstChild("HumanoidRootPart")
if not raiz or not alvo then
return false, "sem posicao para comparar"
end
if (raiz.Position - alvo.Position).Magnitude > distanciaMaxima then
return false, "longe demais do alvo"
end
return true
end
-- ── camada 4: taxa ──────────────────────────────────────────────────
local ultimoUso = {}
function Validacao.taxa(player, chave, intervalo)
local agora = os.clock()
ultimoUso[player] = ultimoUso[player] or {}
local anterior = ultimoUso[player][chave]
if anterior and (agora - anterior) < intervalo then
return false, "rapido demais"
end
ultimoUso[player][chave] = agora
return true
end
game:GetService("Players").PlayerRemoving:Connect(function(player)
ultimoUso[player] = nil
end)
return Validacao


