Loading...
Loading...
Server-authoritative networking, RemoteEvent validation, rate limiting, exploit prevention, security hardening.
npx skill4agent add tabooharmony/roblox-brain roblox-networkingtStarterPlayerScriptsStarterGuiReplicatedStorageReplicatedFirstRemoteEvent:FireServer()**For runtime type checking, thelibrary (osyrisrblx/t v3.1.1, MIT) provides composable type checks (t,t.string,t.number) that are cleaner than manual typeof() chains. Install via Wally or copy the module directly.t.interface({...})
-- BAD: No validation at all
DamageRemote.OnServerEvent:Connect(function(player, targetName, damage)
local target = Players:FindFirstChild(targetName)
target.Character.Humanoid:TakeDamage(damage)
end)ServerScriptService-- ServerScriptService/Modules/RemoteValidator.luau
local RemoteValidator = {}
--[[ -----------------------------------------------------------------------
Type Checking
Validates that arguments match expected types.
----------------------------------------------------------------------- ]]
type TypeSpec = string | (value: any) -> boolean
function RemoteValidator.checkType(value: any, expected: TypeSpec): boolean
if typeof(expected) == "function" then
return expected(value)
end
return typeof(value) == expected
end
function RemoteValidator.validateArgs(
args: { any },
schema: { { name: string, type: TypeSpec, optional: boolean? } }
): (boolean, string?)
for i, spec in schema do
local value = args[i]
if value == nil then
if not spec.optional then
return false, `Missing required argument: {spec.name}`
end
continue
end
if not RemoteValidator.checkType(value, spec.type) then
return false, `Invalid type for {spec.name}: expected {tostring(spec.type)}, got {typeof(value)}`
end
end
-- Reject extra arguments that were not declared in the schema
if #args > #schema then
return false, `Too many arguments: expected {#schema}, got {#args}`
end
return true, nil
end
--[[ -----------------------------------------------------------------------
Range Checking
Validates that numeric values fall within acceptable bounds.
----------------------------------------------------------------------- ]]
function RemoteValidator.checkRange(value: number, min: number, max: number): boolean
return typeof(value) == "number"
and value == value -- NaN check
and value >= min
and value <= max
end
function RemoteValidator.checkIntegerRange(value: number, min: number, max: number): boolean
return RemoteValidator.checkRange(value, min, max)
and math.floor(value) == value
end
--[[ -----------------------------------------------------------------------
Cooldown Tracking
Per-player, per-action cooldown enforcement.
----------------------------------------------------------------------- ]]
local cooldowns: { [Player]: { [string]: number } } = {}
function RemoteValidator.checkCooldown(player: Player, action: string, cooldownSeconds: number): boolean
local now = os.clock()
local playerCooldowns = cooldowns[player]
if not playerCooldowns then
playerCooldowns = {}
cooldowns[player] = playerCooldowns
end
local lastUsed = playerCooldowns[action]
if lastUsed and (now - lastUsed) < cooldownSeconds then
return false
end
playerCooldowns[action] = now
return true
end
function RemoteValidator.clearPlayerCooldowns(player: Player)
cooldowns[player] = nil
end
--[[ -----------------------------------------------------------------------
Existence Checks
Validates that targets, objects, and instances actually exist.
----------------------------------------------------------------------- ]]
function RemoteValidator.playerExists(playerName: string): Player?
local Players = game:GetService("Players")
return Players:FindFirstChild(playerName) :: Player?
end
function RemoteValidator.characterAlive(player: Player): boolean
local character = player.Character
if not character then
return false
end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then
return false
end
return humanoid.Health > 0
end
function RemoteValidator.instanceExists(parent: Instance, name: string, className: string?): Instance?
local child = parent:FindFirstChild(name)
if not child then
return nil
end
if className and not child:IsA(className) then
return nil
end
return child
end
--[[ -----------------------------------------------------------------------
Authorization
Checks if a player is allowed to perform an action.
----------------------------------------------------------------------- ]]
function RemoteValidator.playerOwnsItem(player: Player, itemId: string, inventoryFolder: Folder?): boolean
local folder = inventoryFolder or player:FindFirstChild("Inventory") :: Folder?
if not folder then
return false
end
return folder:FindFirstChild(itemId) ~= nil
end
function RemoteValidator.playerHasAttribute(player: Player, attribute: string, expectedValue: any?): boolean
local value = player:GetAttribute(attribute)
if expectedValue ~= nil then
return value == expectedValue
end
return value ~= nil
end
--[[ -----------------------------------------------------------------------
Distance Check
Validates that two positions are within an acceptable range.
----------------------------------------------------------------------- ]]
function RemoteValidator.withinRange(posA: Vector3, posB: Vector3, maxDistance: number): boolean
return (posA - posB).Magnitude <= maxDistance
end
function RemoteValidator.playerWithinRange(player: Player, targetPos: Vector3, maxDistance: number): boolean
local character = player.Character
if not character then
return false
end
local root = character:FindFirstChild("HumanoidRootPart")
if not root then
return false
end
return RemoteValidator.withinRange(root.Position, targetPos, maxDistance)
end
--[[ -----------------------------------------------------------------------
Cleanup
----------------------------------------------------------------------- ]]
game:GetService("Players").PlayerRemoving:Connect(function(player)
RemoteValidator.clearPlayerCooldowns(player)
end)
return RemoteValidator-- ServerScriptService/RemoteHandlers/DamageHandler.server.luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local Validator = require(ServerScriptService.Modules.RemoteValidator)
local DamageRemote = ReplicatedStorage.Remotes.DealDamage
local MAX_DAMAGE = 50
local DAMAGE_COOLDOWN = 0.5 -- seconds
local ATTACK_RANGE = 15 -- studs
local ARG_SCHEMA = {
{ name = "targetPlayer", type = "Instance" },
{ name = "damage", type = "number" },
}
DamageRemote.OnServerEvent:Connect(function(player: Player, ...: any)
local args = { ... }
-- 1. Validate argument types
local valid, err = Validator.validateArgs(args, ARG_SCHEMA)
if not valid then
warn(`[DamageHandler] {player.Name}: {err}`)
return
end
local targetPlayer: Player = args[1]
local damage: number = args[2]
-- 2. Validate the target is actually a Player
if not targetPlayer:IsA("Player") then
return
end
-- 3. Validate damage range
if not Validator.checkIntegerRange(damage, 1, MAX_DAMAGE) then
warn(`[DamageHandler] {player.Name}: damage out of range ({damage})`)
return
end
-- 4. Cooldown check
if not Validator.checkCooldown(player, "DealDamage", DAMAGE_COOLDOWN) then
return
end
-- 5. Verify attacker is alive
if not Validator.characterAlive(player) then
return
end
-- 6. Verify target is alive
if not Validator.characterAlive(targetPlayer) then
return
end
-- 7. Range check -- attacker must be near the target
local targetRoot = targetPlayer.Character and targetPlayer.Character:FindFirstChild("HumanoidRootPart")
if not targetRoot then
return
end
if not Validator.playerWithinRange(player, targetRoot.Position, ATTACK_RANGE) then
warn(`[DamageHandler] {player.Name}: target out of range`)
return
end
-- 8. Authorization -- verify the player has a weapon equipped
local character = player.Character
local weapon = character and character:FindFirstChildOfClass("Tool")
if not weapon or not weapon:GetAttribute("CanDealDamage") then
warn(`[DamageHandler] {player.Name}: no valid weapon equipped`)
return
end
-- 9. Server calculates actual damage (never trust client damage value directly)
local serverDamage = math.min(damage, weapon:GetAttribute("MaxDamage") or MAX_DAMAGE)
-- 10. Apply damage
local targetHumanoid = targetPlayer.Character:FindFirstChildOfClass("Humanoid")
if targetHumanoid then
targetHumanoid:TakeDamage(serverDamage)
end
end)-- ServerScriptService/Security/MovementValidator.server.luau
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local MAX_SPEED = 50 -- studs per second (walk + sprint + tolerance)
local MAX_VERTICAL_SPEED = 100 -- studs per second (jumping/falling tolerance)
local VIOLATION_THRESHOLD = 5 -- strikes before action
local CHECK_INTERVAL = 0.5 -- seconds between checks
local playerData: { [Player]: {
lastPosition: Vector3,
lastCheck: number,
violations: number,
} } = {}
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function(character)
local root = character:WaitForChild("HumanoidRootPart")
playerData[player] = {
lastPosition = root.Position,
lastCheck = os.clock(),
violations = 0,
}
end)
end)
Players.PlayerRemoving:Connect(function(player)
playerData[player] = nil
end)
RunService.Heartbeat:Connect(function()
local now = os.clock()
for player, data in playerData do
if (now - data.lastCheck) < CHECK_INTERVAL then
continue
end
local character = player.Character
if not character then
continue
end
local root = character:FindFirstChild("HumanoidRootPart")
if not root then
continue
end
local dt = now - data.lastCheck
local displacement = root.Position - data.lastPosition
local horizontalSpeed = Vector3.new(displacement.X, 0, displacement.Z).Magnitude / dt
local verticalSpeed = math.abs(displacement.Y) / dt
if horizontalSpeed > MAX_SPEED or verticalSpeed > MAX_VERTICAL_SPEED then
data.violations += 1
warn(`[MovementValidator] {player.Name}: speed violation #{data.violations} (h={math.floor(horizontalSpeed)}, v={math.floor(verticalSpeed)})`)
if data.violations >= VIOLATION_THRESHOLD then
-- Teleport player back to last valid position
root.CFrame = CFrame.new(data.lastPosition)
-- Or kick for persistent abuse:
-- player:Kick("Movement anomaly detected.")
end
else
-- Decay violations over time for legitimate edge cases
data.violations = math.max(0, data.violations - 1)
data.lastPosition = root.Position
end
data.lastCheck = now
end
end)-- Server decides damage, not the client.
local function calculateDamage(attacker: Player, weapon: Tool, target: Player): number?
local weaponConfig = WeaponDatabase[weapon.Name]
if not weaponConfig then
return nil
end
-- Server checks weapon cooldown
local lastFire = weapon:GetAttribute("LastFired") or 0
if os.clock() - lastFire < weaponConfig.Cooldown then
return nil
end
-- Server checks range
local attackerRoot = attacker.Character and attacker.Character:FindFirstChild("HumanoidRootPart")
local targetRoot = target.Character and target.Character:FindFirstChild("HumanoidRootPart")
if not attackerRoot or not targetRoot then
return nil
end
local distance = (attackerRoot.Position - targetRoot.Position).Magnitude
if distance > weaponConfig.Range then
return nil
end
-- Server calculates damage
weapon:SetAttribute("LastFired", os.clock())
return weaponConfig.BaseDamage
end-- WRONG: Client tells server how much to add
CurrencyRemote.OnServerEvent:Connect(function(player, amount)
player.leaderstats.Gold.Value += amount -- exploiter sends 999999
end)
-- RIGHT: Server calculates the reward
QuestCompleteRemote.OnServerEvent:Connect(function(player, questId)
-- Validate quest ID type
if typeof(questId) ~= "string" then
return
end
-- Server checks quest state
local questData = PlayerQuestData[player]
if not questData or not questData[questId] then
return
end
if questData[questId].completed then
return -- already claimed
end
-- Server looks up the reward from its own data
local questConfig = QuestDatabase[questId]
if not questConfig then
return
end
-- Server awards the reward
questData[questId].completed = true
player.leaderstats.Gold.Value += questConfig.Reward
end)-- Server-side trade validation
local function executeTrade(playerA: Player, playerB: Player, itemIdA: string, itemIdB: string): boolean
-- Both players must be alive and in range
if not Validator.characterAlive(playerA) or not Validator.characterAlive(playerB) then
return false
end
-- Verify ownership on the server
local invA = playerA:FindFirstChild("Inventory")
local invB = playerB:FindFirstChild("Inventory")
if not invA or not invB then
return false
end
---
## Rate Limiting
Roblox's built-in throttle (~500 req/sec per client) is NOT a substitute for custom rate limiting. Players can still spam remotes at hundreds of requests per second. You need application-level throttling.
### Pattern 1: Per-Player Cooldown Table
Simple and effective for most games. Each remote has a minimum time between calls per player.
```luau
local cooldowns: {[Player]: {[string]: number}} = {}
local COOLDOWN = 0.2 -- seconds between calls
local function isThrottled(player: Player, remoteName: string): boolean
local now = os.clock()
if not cooldowns[player] then
cooldowns[player] = {}
end
local lastCall = cooldowns[player][remoteName]
if lastCall and (now - lastCall) < COOLDOWN then
return true -- throttled
end
cooldowns[player][remoteName] = now
return false
end
-- Clean up when player leaves
Players.PlayerRemoving:Connect(function(player)
cooldowns[player] = nil
end)
-- Usage
BuyItem.OnServerEvent:Connect(function(player, itemId)
if isThrottled(player, "BuyItem") then return end
-- process purchase
end)type RemoteDef = {
RateLimit: number?,
Validate: (Player, ...any) -> boolean,
Handler: (Player, ...any) -> (),
}
local Remotes: {[string]: RemoteDef} = {
BuyItem = {
RateLimit = 0.5,
Validate = function(player, itemId)
return typeof(itemId) == "string" and #itemId < 50
end,
Handler = function(player, itemId)
-- process purchase
end,
},
EquipTool = {
RateLimit = 0.3,
Validate = function(player, toolId)
return typeof(toolId) == "string"
end,
Handler = function(player, toolId)
-- equip tool
end,
},
}
-- Wire up automatically
for name, def in Remotes do
local remote = ReplicatedStorage:WaitForChild(name)
remote.OnServerEvent:Connect(function(player, ...)
if def.RateLimit and isThrottled(player, name) then return end
if not def.Validate(player, ...) then return end
def.Handler(player, ...)
end)
endlocal suspicion: {[Player]: number} = {}
local SUSPICION_THRESHOLD = 10
local DECAY_RATE = 1 -- points lost per second
local function addSuspicion(player: Player, amount: number, reason: string)
suspicion[player] = (suspicion[player] or 0) + amount
if suspicion[player] >= SUSPICION_THRESHOLD then
warn(`High suspicion for {player.Name}: {reason}`)
end
end
-- In remote handler
BuyItem.OnServerEvent:Connect(function(player, itemId)
if isThrottled(player, "BuyItem") then
addSuspicion(player, 2, "rate limit exceeded")
return
end
-- normal processing
end)
-- Decay suspicion over time
task.spawn(function()
while true do
task.wait(1)
for player, score in suspicion do
suspicion[player] = math.max(0, score - DECAY_RATE)
end
end
end)-- BAD: no rate limiting at all
BuyItem.OnServerEvent:Connect(function(player, itemId)
-- exploiter can call this 1000 times/second
grantItem(player, itemId)
end)
-- BAD: client-side rate limiting (exploiter bypasses)
-- Rate limiting MUST be server-side