roblox-networking
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese<!-- Source: brockmartin/roblox-game-skill (MIT) -->
<!-- Source: brockmartin/roblox-game-skill (MIT) -->
Roblox Networking & Security Reference
Roblox 网络与安全参考
Overview
概述
Load this reference when:
- Validating RemoteEvent/RemoteFunction input on the server
- Implementing rate limiting or anti-exploit measures
- Designing server-authoritative systems (damage, currency, inventory)
- Hardening existing networking code against exploiters
This document covers server-side validation, rate limiting, suspicion scoring, and server-authoritative design patterns. For player lifecycle (PlayerAdded/Removing), see roblox-architecture.
在以下场景中使用本参考文档:
- 在服务器端验证RemoteEvent/RemoteFunction的输入
- 实现速率限制或反作弊措施
- 设计服务器权威系统(伤害、货币、背包)
- 加固现有网络代码以抵御漏洞利用者
本文档涵盖服务器端验证、速率限制、可疑行为评分以及服务器权威设计模式。有关玩家生命周期(PlayerAdded/Removing)的内容,请参阅roblox-architecture。
Quick Reference
快速参考
Load Full Reference below only when you need specific validation module code or rate limiting implementations.
Key rules:
- NEVER trust the client. Every RemoteEvent arg is attacker-controlled.
- Validate: type, range, ownership, cooldown on EVERY server handler.
- Server-authoritative: server decides outcomes. Client is display-only.
- Rate limit all remotes. Per-player cooldown table minimum.
- Damage: server calculates from weapon stats + distance + cooldown. Never accept damage values from client.
- Currency: all math server-side. Client displays only.
- Movement: validate distance/speed against physics. Flag teleportation.
- Use library for composable type checks on remote args.
t - Suspicion scoring: accumulate violations, kick/ban at threshold. Don't instant-kick on first offense.
- Exploiters can: fire any remote, read all client code, modify any client state, speed/fly/teleport.
仅当你需要特定的验证模块代码或速率限制实现时,才查看下方的完整参考内容。
核心规则:
- 绝不信任客户端。每个RemoteEvent的参数都由攻击者控制。
- 对每个服务器处理程序都要验证:类型、范围、所有权、冷却时间。
- 服务器权威:服务器决定结果。客户端仅负责显示。
- 对所有远程调用进行速率限制。至少使用每个玩家的冷却时间表。
- 伤害:服务器根据武器属性+距离+冷却时间计算。绝不接受客户端传来的伤害值。
- 货币:所有计算在服务器端完成。客户端仅负责显示。
- 移动:验证距离/速度是否符合物理规则。标记瞬移行为。
- 使用库对远程参数进行可组合的类型检查。
t - 可疑行为评分:累计违规次数,达到阈值时踢出/封禁。不要在首次违规时立即踢出。
- 漏洞利用者可以:触发任意远程调用、读取所有客户端代码、修改任意客户端状态、加速/飞行/瞬移。
Full Reference
完整参考
Security Hardening
安全加固
Never Trust the Client
绝不信任客户端
Every RemoteEvent payload is attacker-controlled. Validate type, range, ownership, and cooldown on the server for every request.
- Modify any LocalScript -- injecting code, changing variables, hooking functions.
- Fire any RemoteEvent with arbitrary arguments -- types, values, and counts are all attacker-controlled.
- Speed hack, fly, and teleport -- the character's physics can be overridden entirely on the client.
- See all client-accessible code -- anything in ,
StarterPlayerScripts,StarterGui, orReplicatedStorageis fully readable.ReplicatedFirst - Read and modify any client-side state -- health displays, cooldown timers, UI flags.
- Intercept and replay network traffic -- RemoteSpy tools let exploiters see every remote call and replay or modify them.
The client is a display layer, not a trusted authority. It renders the world and collects input. The server decides what actually happens.
A useful mental model: treat every call as if it were an HTTP request from an anonymous stranger on the internet. Validate everything. Assume nothing.
RemoteEvent:FireServer()每个RemoteEvent的负载都由攻击者控制。对于每个请求,服务器都要验证类型、范围、所有权和冷却时间。
- 修改任意LocalScript -- 注入代码、修改变量、挂钩函数。
- 使用任意参数触发任意RemoteEvent -- 参数的类型、值和数量都由攻击者控制。
- 加速、飞行和瞬移 -- 角色的物理状态可以在客户端被完全覆盖。
- 查看所有客户端可访问的代码 -- 、
StarterPlayerScripts、StarterGui或ReplicatedStorage中的所有内容都可以被完全读取。ReplicatedFirst - 读取和修改任意客户端状态 -- 生命值显示、冷却计时器、UI标记。
- 拦截和重放网络流量 -- RemoteSpy工具允许漏洞利用者查看所有远程调用,并进行重放或修改。
**客户端只是显示层,不是可信的权威节点。**它负责渲染世界和收集输入,服务器才决定实际发生的事情。
一个有用的思维模型:将每个调用视为来自互联网上匿名陌生人的HTTP请求。验证所有内容,不做任何假设。
RemoteEvent:FireServer()RemoteEvent Validation Patterns
RemoteEvent验证模式
**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({...})
**对于运行时类型检查,库(osyrisrblx/t v3.1.1,MIT协议)提供了可组合的类型检查(t、t.string、t.number),比手动的typeof()链式检查更简洁。可通过Wally安装或直接复制模块。t.interface({...})
The Problem
问题示例
A bare remote handler like this is exploitable:
luau
-- BAD: No validation at all
DamageRemote.OnServerEvent:Connect(function(player, targetName, damage)
local target = Players:FindFirstChild(targetName)
target.Character.Humanoid:TakeDamage(damage)
end)An exploiter can fire this with any target name and any damage value, instantly killing anyone.
像这样的裸远程处理程序存在被利用的风险:
luau
-- 错误示例:完全没有验证
DamageRemote.OnServerEvent:Connect(function(player, targetName, damage)
local target = Players:FindFirstChild(targetName)
target.Character.Humanoid:TakeDamage(damage)
end)漏洞利用者可以使用任意目标名称和伤害值触发该事件,瞬间杀死任何人。
Production-Ready Validation Module
生产级验证模块
Place this in :
ServerScriptServiceluau
-- 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将此模块放置在中:
ServerScriptServiceluau
-- ServerScriptService/Modules/RemoteValidator.luau
local RemoteValidator = {}
--[[ -----------------------------------------------------------------------
类型检查
验证参数是否匹配预期类型。
----------------------------------------------------------------------- ]]
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
-- 拒绝未在 schema 中声明的额外参数
if #args > #schema then
return false, `Too many arguments: expected {#schema}, got {#args}`
end
return true, nil
end
--[[ -----------------------------------------------------------------------
范围检查
验证数值是否在可接受的范围内。
----------------------------------------------------------------------- ]]
function RemoteValidator.checkRange(value: number, min: number, max: number): boolean
return typeof(value) == "number"
and value == value -- NaN 检查
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
--[[ -----------------------------------------------------------------------
冷却时间跟踪
针对每个玩家、每个操作执行冷却时间限制。
----------------------------------------------------------------------- ]]
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
--[[ -----------------------------------------------------------------------
存在性检查
验证目标、对象和实例是否真实存在。
----------------------------------------------------------------------- ]]
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
--[[ -----------------------------------------------------------------------
权限验证
检查玩家是否被允许执行某个操作。
----------------------------------------------------------------------- ]]
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
--[[ -----------------------------------------------------------------------
距离检查
验证两个位置是否在可接受的范围内。
----------------------------------------------------------------------- ]]
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
--[[ -----------------------------------------------------------------------
清理
----------------------------------------------------------------------- ]]
game:GetService("Players").PlayerRemoving:Connect(function(player)
RemoteValidator.clearPlayerCooldowns(player)
end)
return RemoteValidatorUsing the Validation Module
使用验证模块
luau
-- 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)luau
-- 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 -- 秒
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. 验证参数类型
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. 验证目标确实是Player实例
if not targetPlayer:IsA("Player") then
return
end
-- 3. 验证伤害值范围
if not Validator.checkIntegerRange(damage, 1, MAX_DAMAGE) then
warn(`[DamageHandler] {player.Name}: damage out of range ({damage})`)
return
end
-- 4. 冷却时间检查
if not Validator.checkCooldown(player, "DealDamage", DAMAGE_COOLDOWN) then
return
end
-- 5. 验证攻击者存活
if not Validator.characterAlive(player) then
return
end
-- 6. 验证目标存活
if not Validator.characterAlive(targetPlayer) then
return
end
-- 7. 距离检查 -- 攻击者必须靠近目标
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. 权限验证 -- 验证玩家已装备武器
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. 服务器计算实际伤害(绝不直接信任客户端传来的伤害值)
local serverDamage = math.min(damage, weapon:GetAttribute("MaxDamage") or MAX_DAMAGE)
-- 10. 施加伤害
local targetHumanoid = targetPlayer.Character:FindFirstChildOfClass("Humanoid")
if targetHumanoid then
targetHumanoid:TakeDamage(serverDamage)
end
end)Server-Authoritative Design
服务器权威设计
The server owns all game state. The client requests actions; the server decides outcomes.
服务器拥有所有游戏状态。客户端请求操作,服务器决定结果。
Movement Validation
移动验证
luau
-- 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)luau
-- ServerScriptService/Security/MovementValidator.server.luau
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local MAX_SPEED = 50 -- 每秒 studs(行走+冲刺+容错)
local MAX_VERTICAL_SPEED = 100 -- 每秒 studs(跳跃/下落容错)
local VIOLATION_THRESHOLD = 5 -- 触发操作的违规次数
local CHECK_INTERVAL = 0.5 -- 检查间隔(秒)
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
-- 将玩家传送回上一个有效位置
root.CFrame = CFrame.new(data.lastPosition)
-- 或者针对持续违规踢出玩家:
-- player:Kick("Movement anomaly detected.")
end
else
-- 随着时间衰减违规次数,处理合法的边缘情况
data.violations = math.max(0, data.violations - 1)
data.lastPosition = root.Position
end
data.lastCheck = now
end
end)Damage Validation
伤害验证
luau
-- 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
endluau
-- 由服务器决定伤害值,而非客户端。
local function calculateDamage(attacker: Player, weapon: Tool, target: Player): number?
local weaponConfig = WeaponDatabase[weapon.Name]
if not weaponConfig then
return nil
end
-- 服务器检查武器冷却时间
local lastFire = weapon:GetAttribute("LastFired") or 0
if os.clock() - lastFire < weaponConfig.Cooldown then
return nil
end
-- 服务器检查距离
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
-- 服务器计算伤害
weapon:SetAttribute("LastFired", os.clock())
return weaponConfig.BaseDamage
endCurrency Transactions
货币交易
luau
-- 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)luau
-- 错误示例:客户端告知服务器要添加的金额
CurrencyRemote.OnServerEvent:Connect(function(player, amount)
player.leaderstats.Gold.Value += amount -- 漏洞利用者可以传入999999
end)
-- 正确示例:服务器计算奖励
QuestCompleteRemote.OnServerEvent:Connect(function(player, questId)
-- 验证任务ID类型
if typeof(questId) ~= "string" then
return
end
-- 服务器检查任务状态
local questData = PlayerQuestData[player]
if not questData or not questData[questId] then
return
end
if questData[questId].completed then
return -- 已领取奖励
end
-- 服务器从自身数据中查找奖励
local questConfig = QuestDatabase[questId]
if not questConfig then
return
end
-- 服务器发放奖励
questData[questId].completed = true
player.leaderstats.Gold.Value += questConfig.Reward
end)Inventory Operations
背包操作
luau
-- 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
---luau
-- 服务器端交易验证
local function executeTrade(playerA: Player, playerB: Player, itemIdA: string, itemIdB: string): boolean
-- 两名玩家必须存活且在范围内
if not Validator.characterAlive(playerA) or not Validator.characterAlive(playerB) then
return false
end
-- 在服务器端验证所有权
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.
Roblox内置的限流(每个客户端约500请求/秒)不能替代自定义速率限制。玩家仍然可以以每秒数百次的频率发送远程调用。你需要应用级别的限流措施。
Pattern 1: Per-Player Cooldown Table
模式1:每个玩家的冷却时间表
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)简单有效,适用于大多数游戏。每个远程调用对每个玩家都有最小调用间隔。
luau
local cooldowns: {[Player]: {[string]: number}} = {}
local COOLDOWN = 0.2 -- 调用间隔(秒)
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 -- 被限流
end
cooldowns[player][remoteName] = now
return false
end
-- 玩家离开时清理
Players.PlayerRemoving:Connect(function(player)
cooldowns[player] = nil
end)
-- 使用示例
BuyItem.OnServerEvent:Connect(function(player, itemId)
if isThrottled(player, "BuyItem") then return end
-- 处理购买逻辑
end)Pattern 2: Declarative Remote Definitions
模式2:声明式远程调用定义
Define all remotes in one place with rate limits, validation, and allowed states. Cleaner than scattered OnServerEvent handlers.
luau
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)
end在一个地方定义所有远程调用,包含速率限制、验证和允许的状态。比分散的OnServerEvent处理程序更整洁。
luau
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)
-- 处理购买逻辑
end,
},
EquipTool = {
RateLimit = 0.3,
Validate = function(player, toolId)
return typeof(toolId) == "string"
end,
Handler = function(player, toolId)
-- 装备工具
end,
},
}
-- 自动关联
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)
endPattern 3: Suspicion Scoring
模式3:可疑行为评分
For high-stakes games. Track suspicious behavior over time instead of hard-blocking.
luau
local 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)适用于高风险游戏。跟踪一段时间内的可疑行为,而非直接拦截。
luau
local suspicion: {[Player]: number} = {}
local SUSPICION_THRESHOLD = 10
local DECAY_RATE = 1 -- 每秒减少的分数
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
-- 在远程处理程序中使用
BuyItem.OnServerEvent:Connect(function(player, itemId)
if isThrottled(player, "BuyItem") then
addSuspicion(player, 2, "rate limit exceeded")
return
end
-- 正常处理逻辑
end)
-- 随着时间衰减可疑分数
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)What NOT to Do
错误做法
luau
-- 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-sideSource: Roblox Server-Side Detection Guide (Roblox/creator-docs, MIT), DevForum rate limiting patterns
luau
-- 错误示例:完全没有速率限制
BuyItem.OnServerEvent:Connect(function(player, itemId)
-- 漏洞利用者可以每秒调用1000次
grantItem(player, itemId)
end)
-- 错误示例:客户端速率限制(漏洞利用者可以绕过)
-- 速率限制必须在服务器端实现资料来源:Roblox服务器端检测指南(Roblox/creator-docs,MIT协议)、开发者论坛限流模式