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
    t
    library for composable type checks on remote args.
  • 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
    ,
    ReplicatedStorage
    , or
    ReplicatedFirst
    is fully readable.
  • 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
RemoteEvent:FireServer()
call as if it were an HTTP request from an anonymous stranger on the internet. Validate everything. Assume nothing.

每个RemoteEvent的负载都由攻击者控制。对于每个请求,服务器都要验证类型、范围、所有权和冷却时间。
  • 修改任意LocalScript -- 注入代码、修改变量、挂钩函数。
  • 使用任意参数触发任意RemoteEvent -- 参数的类型、值和数量都由攻击者控制。
  • 加速、飞行和瞬移 -- 角色的物理状态可以在客户端被完全覆盖。
  • 查看所有客户端可访问的代码 --
    StarterPlayerScripts
    StarterGui
    ReplicatedStorage
    ReplicatedFirst
    中的所有内容都可以被完全读取。
  • 读取和修改任意客户端状态 -- 生命值显示、冷却计时器、UI标记。
  • 拦截和重放网络流量 -- RemoteSpy工具允许漏洞利用者查看所有远程调用,并进行重放或修改。
**客户端只是显示层,不是可信的权威节点。**它负责渲染世界和收集输入,服务器才决定实际发生的事情。
一个有用的思维模型:将每个
RemoteEvent:FireServer()
调用视为来自互联网上匿名陌生人的HTTP请求。验证所有内容,不做任何假设。

RemoteEvent Validation Patterns

RemoteEvent验证模式

**For runtime type checking, the
t
library (osyrisrblx/t v3.1.1, MIT) provides composable type checks (
t.string
,
t.number
,
t.interface({...})
) that are cleaner than manual typeof() chains. Install via Wally or copy the module directly.
**对于运行时类型检查,
t
库(osyrisrblx/t v3.1.1,MIT协议)提供了可组合的类型检查(
t.string
t.number
t.interface({...})
),比手动的typeof()链式检查更简洁。可通过Wally安装或直接复制模块。

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
ServerScriptService
:
luau
-- 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
中:
luau
-- 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 RemoteValidator

Using 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
end
luau
-- 由服务器决定伤害值,而非客户端。

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
end

Currency 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)
end

Pattern 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-side
Source: 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协议)、开发者论坛限流模式