Cada vez que un cliente puede activar una acción en el servidor, hay potencial para el abuso. Aunque RemoteEvents y RemoteFunctions son los vectores de ataque más comunes aquí, otras instancias como ProximityPrompt son susceptibles. Esta sección cubre cómo asegurar este límite.
Validando entradas remotas
Cada pieza de datos enviada desde un cliente debe ser validada por el servidor antes de ser utilizada. Aplica estas capas de validación dependiendo de lo que haga la remota.
Validación de contexto/permisos
Esto es necesario para remotos que afectan el estado de la experiencia, la progresión, u otros jugadores. El servidor debe verificar si el jugador tiene los permisos necesarios para hacer la solicitud. Por ejemplo, ¿está el jugador lo suficientemente cerca de una tienda para comprar algo? ¿Tienen una llave necesaria para abrir una puerta? ¿Está su personaje vivo?
La validación es necesaria para remotos que:
- Conceden recompensas o modifican la progresión del jugador.
- Afectan a otros jugadores o al estado compartido de la experiencia.
- Realizan acciones con requisitos de distancia, tiempo o permisos (por ejemplo, distancia de la tienda).
Validación de tipo y estructura
Otro ataque que los explotadores podrían lanzar es enviar tipos técnicamente válidos pero hacerlos extremadamente grandes, largos o de otra manera mal formados. Por ejemplo, si el servidor tiene que realizar una operación costosa en una cadena que escala con la longitud, un explotador podría enviar una cadena increíblemente grande o mal formada para sobrecargar el servidor.
Otro ataque común que los explotadores pueden usar implica enviar tables en lugar de una Instance. Las cargas útiles complejas pueden imitar lo que sería una referencia de objeto de otro modo ordinaria.
Por ejemplo, proporcionado con un sistema de tienda dentro de la experiencia donde los datos de los artículos como precios son almacenados en objetos NumberValue, un explotador puede eludir todas las demás verificaciones haciendo lo siguiente:
LocalScript en 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, -- ¡Los valores negativos también podrían usarse, resultando en dar moneda en lugar de tomarla!},}-- Enviar carga útil maliciosa al servidor (esto será rechazado)print(buyItemEvent:InvokeServer(payload)) -- Salida "false Invalid item provided"-- Enviar un artículo real al servidor (esto será aceptado!)print(buyItemEvent:InvokeServer(itemDataFolder["Real Blade"])) -- Salida "true" y la moneda restante si la compra tiene éxito
Script en ServerScriptStorage
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local itemDataFolder = ReplicatedStorage:WaitForChild("ItemData")
local buyItemEvent = ReplicatedStorage:WaitForChild("BuyItemEvent")
local function buyItem(player, item)
-- Verificar que el artículo pasado no está alterado y está en la carpeta ItemData
if typeof(item) ~= "Instance" or not item:IsDescendantOf(itemDataFolder) then
return false, "Invalid item provided"
end
-- El servidor puede entonces continuar procesando la compra según el flujo de ejemplo a continuación
end
-- Vincular "buyItem()" al callback de la función remota
buyItemEvent.OnServerInvoke = buyItem
Validación de valores
Además de validar tipos y datos, debes validar los valores pasados a través de RemoteEvents y RemoteFunctions, asegurando que sean válidos y lógicos en el contexto que se está solicitando. Esto es esencial para remotos que manejan moneda, artículos, entradas numéricas o contenido generado por el usuario. Debes asegurar que los valores estén dentro de límites esperados (p. ej., una cantidad de compra es mayor que cero) y que los IDs o nombres proporcionados sean válidos (p. ej., un itemId corresponde a un artículo real en tu juego).
Consideraciones especiales para valores numéricos: Tanto inf como NaN son tipos de números válidos, pero ambos pueden causar problemas importantes si un explotador los envía y no se manejan correctamente. Utiliza funciones como estas para detectarlos y rechazarlos:
local function isNaN(n: number): boolean
-- NaN nunca es igual a sí mismo
return n ~= n
end
local function isInf(n: number): boolean
-- El número podría ser -inf o +inf
return math.abs(n) == math.huge
end
El peligro de NaN
Un explotador puede enviar NaN (No es un número) como un argumento. NaN es especialmente peligroso porque es del tipo "número" pero falla todas las comparaciones estándar, lo que permite eludir sutilmente los chequeos lógicos que parecen seguros. Veamos un sistema de comercio vulnerable donde un jugador hace una oferta.
-- CÓDIGO DEL SERVIDOR VULNERABLE
local function onCreateTradeOffer(player, offeredGold)
-- 1. CHEQUEO DE TIPO: ¡Esto pasa! typeof(NaN) es "number".
if typeof(offeredGold) ~= "number" then
return "Invalid offer"
end
-- 2. CHEQUEO DE RANGO: ¡Esto es eludido!
-- (NaN < 0) es falso. (NaN > 1000000) también es falso. El chequeo no hace nada.
if offeredGold < 0 or offeredGold > 1000000 then
return "Offer out of range"
end
-- 3. CHEQUEO DE INVENTARIO: ¡Esto es eludido!
-- (NaN > player.Gold.Value) es falso.
if offeredGold > player.Gold.Value then
return "Not enough gold"
end
-- VULNERABILIDAD: ¡Se crea una oferta de comercio fraudulenta con oro NaN!
createTrade(player, {gold = offeredGold})
return "Trade offer created."
end
El explotador creó con éxito una oferta de comercio sin tener oro, porque cada chequeo falló en silencio. Además, este único valor NaN ahora envenenará cualquier otro sistema que intente usarlo, e incluso se difundiría a otros jugadores a través de la oferta.
A continuación, algunos ejemplos generales que cubren el diseño seguro de características comunes en el juego.
Tienda dentro de la experiencia
Considera un sistema de tienda dentro de la experiencia con una interfaz de usuario, por ejemplo, un menú de selección de productos con un botón de "Comprar". Cuando se presiona el botón, puedes invocar una RemoteFunction entre el cliente y el servidor para solicitar la compra. Sin embargo, es importante que el servidor, el administrador más confiable de la experiencia, confirme que el usuario tiene suficiente dinero para comprar el artículo.

Apuntando armas
Los escenarios de combate requieren atención especial en la validación de valores, particularmente a través de la puntería y la validación de hits.
Imagina una experiencia donde un jugador puede disparar un rayo láser a otro jugador. En lugar de que el cliente le diga al servidor a quién dañar, debería en su lugar informar al servidor la posición de origen del disparo y la parte/posición que cree que ha golpeado. El servidor puede luego validar lo siguiente:
- La posición desde la que el cliente informa que está disparando está cerca del personaje del jugador en el servidor. Ten en cuenta que el servidor y el cliente diferirán ligeramente debido a latencia, así que se necesitará aplicar una tolerancia extra.
- La posición que el cliente informa como golpeada está razonablemente cerca de la posición de la parte que el cliente informa haber golpeado, en el servidor.
- No hay obstrucciones estáticas entre la posición desde la que el cliente informa que está disparando y la posición a la que el cliente informa que está disparando. Este chequeo asegura que un cliente no esté intentando disparar a través de paredes. Ten en cuenta que esto solo debe verificar geometría estática para evitar que disparos válidos sean rechazados debido a latencia.
Además, puede ser que desees implementar validaciones adicionales del lado del servidor como las siguientes:
- Rastrear cuándo fue la última vez que el jugador disparó su arma y validar para asegurarse de que no está disparando demasiado rápido.
- Rastrear la cantidad de municiones de cada jugador en el servidor y confirmar que un jugador que dispara tiene suficiente munición para ejecutar el ataque con el arma.
- Si has implementado equipos o un sistema de combate de "jugadores contra bots", confirmar que el personaje golpeado es un enemigo, no un compañero.
- Confirmar que el jugador golpeado está vivo.
- Almacenar el estado del arma y del jugador en el servidor y confirmar que un jugador que dispara no está bloqueado por una acción actual como recargar o un estado como correr.
Limitación de tasa
Siempre que la lógica del lado del servidor pueda ser activada por el cliente (a través de remotos, eventos Touched, indicaciones de proximidad, ClickDetectors, etc.), existe la posibilidad de que esta lógica pueda ser spameada por explotadores o incluso usuarios legítimos. La limitación de tasa controla con qué frecuencia se pueden realizar estas acciones, previniendo abusos y la sobrecarga del sistema. Si bien es práctico (y a veces necesario) implementar límites de tasa en el cliente, nunca debes confiar únicamente en un límite de tasa del lado del cliente.
Para cualquier lógica del lado del servidor que pueda ser activada por un cliente, siempre considera la tasa máxima a la que dicha lógica debería ejecutarse. Muchas acciones del lado del servidor deben tener su frecuencia limitada para prevenir abusos o fallas, tales como:
- Llamadas a la API de Roblox: APIs de backend como DataStoreService y BadgeService tienen límites de tasa incorporados que harán que tus solicitudes fallen si se superan.
- Operaciones computacionalmente costosas: Acciones que consumen recursos significativos del servidor o afectan a otros clientes.
- Ejemplo del servidor: clonar modelos grandes desde ServerStorage, incluso si nunca se replican a los clientes.
- Ejemplo del cliente: instruir a todos los clientes conectados para actualizar su GUI simultáneamente.
- Mecánicas explotables: Acciones que podrían ser abusadas si se realizan rápidamente, tales como conceder invulnerabilidad, teletransportar jugadores, o premiar moneda.
Ejemplo de cubo de tokens
Un enfoque robusto y común para la limitación de tasa es el algoritmo cubo de tokens. Imagina que cada usuario tiene un cubo que puede contener un cierto número de tokens. Para realizar una acción, el usuario debe gastar un token. El cubo se llena con nuevos tokens a una tasa constante. Este método permite ráfagas cortas de acciones cuando sea necesario, pero previene el spam sostenido al imponer una tasa promedio a lo largo del tiempo.
--!strict
-- Módulo ubicado en 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 limitador que permite como máximo `capacity` eventos, rellenando de nuevo a
-- completo a una tasa de `capacity / windowSeconds` tokens por segundo.
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
-- Devuelve falso si el usuario excedería su límite
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 el módulo, primero creas una nueva instancia limitadora con TokenBucket.new(capacity, windowSeconds). La capacity es el número máximo de solicitudes que un usuario puede hacer en una ráfaga rápida, y el windowSeconds determina cuánto tiempo tarda en rellenar todos esos tokens. Por ejemplo, TokenBucket.new(5, 10) crea un limitador que permite ráfagas de hasta 5 solicitudes y rellena un token cada dos segundos (10 segundos / 5 tokens).
Antes de ejecutar lógica protegida, haz una llamada al método :allow(userId), denegando la acción si devuelve falso. También es una buena práctica limpiar los datos de la cubeta del usuario cuando se va para evitar fugas de memoria.
El siguiente script de ejemplo demuestra cómo proteger una RemoteEvent utilizada para un sistema de chat personalizado de ser spammeada por jugadores.
-- Ejemplo de uso en un script del 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 mensajes por 10 segundos (capacidad 5, se rellena a 0.5 tokens/segundo)
local chatLimiter = TokenBucket.new(5, 10)
ChatRemote.OnServerEvent:Connect(function(player: Player, message: string)
if not chatLimiter:allow(player.UserId) then
-- Demasiado rápido: descartar la solicitud o enviar una advertencia de spam
return
end
-- Procesar el mensaje (después de otra validación)
broadcastMessage(player, message)
end)
-- Limpieza para evitar fugas de memoria
Players.PlayerRemoving:Connect(function(p: Player)
chatLimiter.buckets[p.UserId] = nil
end)
Asegurando instancias activadas por el cliente
ProximityPrompts, ClickDetectors, y DragDetectors no son remotos, pero un explotador puede activar sus eventos desde cualquier distancia, en cualquier momento, a menudo ignorando propiedades como Enabled o MaxActivationDistance. Estos objetos deben ser asegurados con el mismo rigor que los remotos.
ProximityPrompt
- Un explotador puede disparar cualquier evento incluso si Enabled es falso en el servidor.
- Enabled, MaxActivationDistance, y RequiresLineOfSight pueden ser cambiados silenciosamente en el cliente, así que cualquier ProximityPrompt replicado puede ser visto e interactuado potencialmente desde cualquier ubicación.
- El servidor aceptará eventos de retención (como PromptButtonHoldBegan) incluso si el HoldDuration del servidor es cero. El cliente puede manipular el HoldDuration para realizar interacciones de manera inusualmente rápida.
- Solo el evento Triggered tiene un chequeo de distancia del lado del servidor. Otros eventos (como PromptButtonHoldBegan y TriggerEnded) no tienen chequeo de distancia, y pueden ser enviados arbitrariamente por cualquier cliente.
ClickDetector
- No hay chequeos en absoluto en el servidor para ningún evento.
- Los explotadores pueden replicar cualquier evento a cualquier distancia, incluso si el ClickDetector está deshabilitado (MaxActivationDistance de 0 o DragDetector.Enabled falso).
- Los eventos pueden ser replicados incluso si el ClickDetector no está parentado a algo clickable.
DragDetector
- Para eventos heredados de ClickDetector, no hay chequeos.
- Para eventos de arrastre, las propiedades Enabled y PermissionPolicy son chequeadas y respetadas por el servidor. Todas las otras propiedades no son chequeadas.
Cuando uno de estos eventos se dispara, tu script del servidor debe realizar su propia validación antes de realizar cualquier acción:
- Verificar si está habilitado: Asegúrate de que la instancia esté diseñada para habilitarse verificando sus propiedades (como Enabled, HoldDuration, RunLocally, etc.) en el servidor. Este paso no es necesario si el objeto siempre está habilitado.
- Verificar el estado del jugador: Asegúrate de que el jugador pueda realizar la acción, dado el estado actual del jugador. ¿Está el personaje del jugador en el mundo? ¿Está lo suficientemente cerca del objeto? ¿Debería el jugador estar vivo para interactuar con este objeto? Verifica cualquier otro estado del jugador mantenido por tu experiencia en el servidor según sea necesario.
- Aplicar limitación de tasa: Como se cubrió en Limitación de tasa, aplica enfriamientos en el cliente y aplica límites de tasa en el servidor para evitar que estos eventos sean spameados y abusados. Para interacciones que se espera que tomen un tiempo mínimo, asegúrate de que los clientes no las completen demasiado rápido.
Nota sobre la propiedad de red: Por defecto, si cualquiera de estos objetos son hijos de partes no ancladas o de un ensamblaje no anclado, un explotador puede tomar la propiedad de red de las partes padre y moverlas directamente a su personaje, eludiendo chequeos de distancia. Para acciones críticas, utiliza partes ancladas o la API de propiedad de red junto con la validación necesaria del lado del servidor.
RemoteEvents y RemoteFunctions
Limita el alcance y el impacto de RemoteFunctions y RemoteEvents. Ten un cuidado extremo al cargar dinámicamente cualquier recurso (incluso texturas o sonidos) o al ejecutar código de experiencia (especialmente a través de require) basado en los argumentos de funciones/eventos remotos. Evita implementar remotos que permitan a un cliente especificar una ruta o referencia de instancia arbitraria para que el servidor elimine o modifique, incluso si tales modificaciones parecen triviales. Los remotos que pueden cambiar el estado de una instancia arbitraria o hacer cambios generalizados en el árbol DataModel a menudo pueden ser encadenados con otros errores para afectar severamente el estado general o la lógica. Verifica no solo el tipo de argumentos de instancia, sino también la clase y la ubicación o estructura esperada dentro del DataModel.
Comunicación de cliente a cliente
Algunos remotos están diseñados para permitir que un cliente active efectos en otros clientes. Esto ocurre cuando un cliente dispara un remoto al servidor, que luego utiliza RemoteEvent:FireAllClients(), RemoteEvent:FireClient(), o métodos similares para retransmitir información. Este patrón es peligroso si no se asegura adecuadamente: el servidor debe ser un controlador, no solo un relé.
Imagina un escenario donde los jugadores pueden lanzar un hechizo de rayo. Cuando se lanza, los jugadores cercanos ven el destello, oyen el sonido y experimentan un temblor de cámara.
Un script del servidor vulnerable podría verse así:
Código del servidor vulnerable (script en ServerScriptStorage)
local castLightningEvent = game.ReplicatedStorage.CastLightning
-- Este script del servidor retransmite el mensaje a todos sin validación
castLightningEvent.OnServerEvent:Connect(function(player, strikePosition)
castLightningEvent:FireAllClients(strikePosition)
end)
Código del efecto en el cliente
local castLightningEvent = game.ReplicatedStorage.CastLightning
-- Crear efectos visuales y de sonido en el cliente
local function createLightningEffect(strikePosition)
-- Código para sacudir la cámara
-- Código para reproducir un fuerte sonido de rayo
-- Código para crear un rayo visual
print("Rayo cae en: " .. tostring(strikePosition))
end
-- Cuando el servidor transmite el evento, este cliente ejecuta el efecto
castLightningEvent.OnClientEvent:Connect(createLightningEffect)
Un explotador puede abusar de este sistema vulnerable al:
- Spamear: Llamando al remoto cientos de veces por segundo, causando efectos continuos que hacen que la experiencia sea injugable.
- Datos inválidos: Enviando nil, NaN, o tipos incorrectos que causan errores de script en todos los clientes.
- Uso no autorizado: Lanzando hechizos que no han desbloqueado o no tienen recursos para.
El servidor debe ser un controlador, no un relé. Antes de retransmitir, debe validar la solicitud y aplicar limitaciones de tasa por jugador. Aquí hay un ejemplo de algunos métodos que podrían emplearse para validar la solicitud.
Código seguro del servidor
local castLightningEvent = game.ReplicatedStorage.CastLightning
local playerCooldowns = {}
local COOLDOWN_TIME = 3
castLightningEvent.OnServerEvent:Connect(function(player, strikePosition)
-- 1. Validación de tipo
if typeof(strikePosition) ~= "Vector3" then
return -- Tipo inválido, rechazar en silencio
end
-- 2. Comprobar por NaN (NaN ~= NaN es la única forma de detectarlo)
if strikePosition.X ~= strikePosition.X then
return -- Contiene NaN, rechazar
end
-- 3. Ejemplo de limitación de tasa
local lastCast = playerCooldowns[player] or 0
if tick() - lastCast < COOLDOWN_TIME then
return -- Aún en tiempo de enfriamiento
end
-- 4. Ejemplo de chequeo de permiso
if not player:GetAttribute("HasLightningSpell") then
return -- El jugador no tiene este hechizo
end
-- 5. Ejemplo de validación de rango
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 -- Fuera de rango
end
-- Todos los chequeos aprobados - seguro para retransmitir
playerCooldowns[player] = tick()
castLightningEvent:FireAllClients(player, strikePosition)
end)
Al implementar estas verificaciones, se hace más difícil para un explotador arruinar la experiencia para otros jugadores. El servidor actúa como una barrera protectora, asegurando que solo se retransmitan acciones válidas y autorizadas a otros clientes.
Consideraciones especiales
Los siguientes son algunos casos adicionales que deberías considerar y que pueden requerir un manejo especializado.
Manipulación del almacén de datos
En experiencias que utilizan DataStoreService para guardar datos de los jugadores, los explotadores pueden aprovechar datos inválidos, condiciones de carrera para corromper salvados o duplicar elementos en un DataStore. Esto es especialmente problemático en experiencias con comercio de artículos, mercados y sistemas de moneda.
Asegúrate de que cualquier acción realizada a través de un RemoteEvent o RemoteFunction que afecte los datos del jugador con entradas del cliente sea sanitizada según lo siguiente:
- Los valores de instancia no pueden ser serializados en un DataStore y fallarán. Utiliza validación de tipo para prevenir esto.
- Los DataStores tienen límites de datos. Las cadenas de longitud arbitraria deben ser chequeadas y/o limitadas para evitar esto, junto con asegurarse de que no se puedan agregar claves arbitrarias ilimitadas a tablas por parte del cliente.
- Los índices de tabla no pueden ser NaN o nil. Itera sobre todas las tablas pasadas por el cliente y verifica que todos los índices sean válidos.
- Los DataStores solo pueden aceptar caracteres UTF-8 válidos, por lo que debes sanitizar todas las cadenas proporcionadas por el cliente a través de utf8.len() para asegurarte de que sean válidas. utf8.len() devolverá la longitud de una cadena, tratando los caracteres unicode como un solo carácter; si se encuentra un carácter UTF-8 inválido, devolverá nil y la posición del carácter inválido. Ten en cuenta que las cadenas UTF-8 inválidas también pueden estar presentes en tablas como claves y valores.
- Explotaciones de números infinitos/NaN - Los explotadores pueden enviar valores como -1/0, 0/0, o math.huge para causar moneda infinita o romper cálculos. Siempre valida utilizando los métodos descritos en el peligro de NaN.
Las condiciones de carrera pueden ocurrir cuando los jugadores manipulan el tiempo entre operaciones para duplicar elementos o corromper datos.
Ejemplo de explotación de comercio: Un jugador inicia un comercio, envía su artículo a otro jugador, y luego se desconecta inmediatamente del juego. Si el comercio se completa pero su salvado en DataStore falla debido a datos inválidos, se vuelve a unir con sus artículos originales mientras que el otro jugador mantiene los artículos comerciados, resultando en duplicación.
Estrategias de prevención:
- Valida todos los datos antes de cualquier operación de comercio.
- Utiliza patrones similares a transacciones donde se valida los datos de todos los jugadores antes de comprometer cualquier cambio.
- Implementa un manejo de errores adecuado que revierta todos los cambios si alguna parte falla.
MarketplaceService
Para interacciones que involucran MarketplaceService, tales como pases o productos de desarrollador, toda la validación de compras y concesión de artículos debe llevarse a cabo en el servidor. Específicamente, utiliza la devolución de llamada ProcessReceipt para validar de forma segura los recibos de compra. Nunca confíes en una señal del lado del cliente, como PromptProductPurchaseFinished, para confirmar una compra sin verificación del servidor, ya que estas pueden ser falsificadas. Asegúrate de que tu función ProcessReceipt revise a fondo los detalles del recibo y solo conceda artículos o moneda después de confirmar una transacción legítima con los servidores de Roblox, y está preparado para manejar casos donde un producto ya ha sido concedido para un recibo dado.