roblox-security

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Roblox Security: Anti-Exploit & Server-Side Validation

Roblox安全:反作弊与服务端验证

Core Principle

核心原则

Never trust the client. Every LocalScript runs on the player's machine and can be modified. All authoritative logic — damage, currency, stats, position changes — must live on the server.
FilteringEnabled is always on in modern Roblox. Client-side changes do not replicate to the server or other clients unless the server explicitly applies them.

永远不要信任客户端。 每个LocalScript都运行在玩家的设备上,并且可以被篡改。所有权威逻辑——伤害计算、货币、统计数据、位置变更——都必须放在服务端执行。
现代Roblox中FilteringEnabled默认始终开启,除非服务端明确应用变更,否则客户端的修改不会同步到服务端或其他客户端。

Secure vs Insecure Patterns

安全与不安全模式对比

PatternInsecureSecure
Dealing damageLocalScript sets
Humanoid.Health
Server reduces health after validation
Awarding currencyLocalScript increments leaderstatsServer validates action, then increments
Leaderstats ownershipLocalScript owns the IntValueServer creates and owns all leaderstats
Position changesLocalScript teleports characterServer validates and moves character
Tool useClient fires damage on hitServer raycasts and applies damage
CooldownsClient tracks cooldown locallyServer tracks cooldown per player

场景不安全做法安全做法
造成伤害LocalScript直接设置
Humanoid.Health
服务端校验后再减少生命值
发放货币LocalScript直接增加leaderstats数值服务端验证行为合规后再增加数值
排行榜数据所有权LocalScript持有IntValue服务端创建并持有所有leaderstats
位置变更LocalScript直接传送角色服务端校验后再移动角色
工具使用客户端命中时触发伤害服务端执行射线检测后施加伤害
冷却时间客户端本地跟踪冷却状态服务端按玩家维度跟踪冷却状态

Secure Leaderstats Setup

安全的排行榜设置

lua
-- Script in ServerScriptService — never LocalScript
game.Players.PlayerAdded:Connect(function(player)
    local leaderstats = Instance.new("Folder")
    leaderstats.Name = "leaderstats"
    leaderstats.Parent = player

    local coins = Instance.new("IntValue")
    coins.Name = "Coins"
    coins.Value = 0
    coins.Parent = leaderstats
end)

lua
-- 脚本放在ServerScriptService中 —— 绝对不能放在LocalScript里
game.Players.PlayerAdded:Connect(function(player)
    local leaderstats = Instance.new("Folder")
    leaderstats.Name = "leaderstats"
    leaderstats.Parent = player

    local coins = Instance.new("IntValue")
    coins.Name = "Coins"
    coins.Value = 0
    coins.Parent = leaderstats
end)

Server-Side Sanity Checks

服务端合理性检查

Distance Check

距离检查

lua
local MAX_INTERACT_DISTANCE = 10

InteractRemote.OnServerEvent:Connect(function(player, targetPart)
    if typeof(targetPart) ~= "Instance" or not targetPart:IsA("BasePart") then return end

    local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
    if not root then return end

    if (root.Position - targetPart.Position).Magnitude > MAX_INTERACT_DISTANCE then
        warn(player.Name .. " sent interaction from invalid distance")
        return
    end

    processInteraction(player, targetPart)
end)
lua
local MAX_INTERACT_DISTANCE = 10

InteractRemote.OnServerEvent:Connect(function(player, targetPart)
    if typeof(targetPart) ~= "Instance" or not targetPart:IsA("BasePart") then return end

    local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
    if not root then return end

    if (root.Position - targetPart.Position).Magnitude > MAX_INTERACT_DISTANCE then
        warn(player.Name .. " 从无效距离发送交互请求")
        return
    end

    processInteraction(player, targetPart)
end)

Cooldown Validation

冷却校验

lua
local ABILITY_COOLDOWN = 5
local lastUsed = {}

UseAbilityRemote.OnServerEvent:Connect(function(player)
    local now = os.clock()
    if now - (lastUsed[player] or 0) < ABILITY_COOLDOWN then return end
    lastUsed[player] = now
    applyAbility(player)
end)

game.Players.PlayerRemoving:Connect(function(player)
    lastUsed[player] = nil
end)
lua
local ABILITY_COOLDOWN = 5
local lastUsed = {}

UseAbilityRemote.OnServerEvent:Connect(function(player)
    local now = os.clock()
    if now - (lastUsed[player] or 0) < ABILITY_COOLDOWN then return end
    lastUsed[player] = now
    applyAbility(player)
end)

game.Players.PlayerRemoving:Connect(function(player)
    lastUsed[player] = nil
end)

Stat Bounds Check

数值边界检查

lua
local MAX_QUANTITY = 99
local ITEM_COST = 50

BuyItemRemote.OnServerEvent:Connect(function(player, quantity)
    if type(quantity) ~= "number" then return end
    quantity = math.clamp(math.floor(quantity), 1, MAX_QUANTITY)

    local coins = player.leaderstats.Coins
    if coins.Value < ITEM_COST * quantity then return end

    coins.Value = coins.Value - (ITEM_COST * quantity)
    -- award items server-side
end)

lua
local MAX_QUANTITY = 99
local ITEM_COST = 50

BuyItemRemote.OnServerEvent:Connect(function(player, quantity)
    if type(quantity) ~= "number" then return end
    quantity = math.clamp(math.floor(quantity), 1, MAX_QUANTITY)

    local coins = player.leaderstats.Coins
    if coins.Value < ITEM_COST * quantity then return end

    coins.Value = coins.Value - (ITEM_COST * quantity)
    -- 服务端发放道具
end)

Rate Limiting

限流

lua
local RATE_LIMIT = 10   -- max calls
local RATE_WINDOW = 1   -- per second
local callLog = {}

local function isRateLimited(player)
    local now = os.clock()
    local log = callLog[player] or {}
    local pruned = {}
    for _, t in ipairs(log) do
        if now - t < RATE_WINDOW then table.insert(pruned, t) end
    end
    if #pruned >= RATE_LIMIT then
        callLog[player] = pruned
        return true
    end
    table.insert(pruned, now)
    callLog[player] = pruned
    return false
end

ActionRemote.OnServerEvent:Connect(function(player)
    if isRateLimited(player) then return end
    handleAction(player)
end)

game.Players.PlayerRemoving:Connect(function(player)
    callLog[player] = nil
end)

lua
local RATE_LIMIT = 10   -- 最大请求次数
local RATE_WINDOW = 1   -- 时间窗口(秒)
local callLog = {}

local function isRateLimited(player)
    local now = os.clock()
    local log = callLog[player] or {}
    local pruned = {}
    for _, t in ipairs(log) do
        if now - t < RATE_WINDOW then table.insert(pruned, t) end
    end
    if #pruned >= RATE_LIMIT then
        callLog[player] = pruned
        return true
    end
    table.insert(pruned, now)
    callLog[player] = pruned
    return false
end

ActionRemote.OnServerEvent:Connect(function(player)
    if isRateLimited(player) then return end
    handleAction(player)
end)

game.Players.PlayerRemoving:Connect(function(player)
    callLog[player] = nil
end)

Argument Validation Utility

参数校验工具

lua
-- ServerScriptService/Modules/Validate.lua
local Validate = {}

function Validate.number(value, min, max)
    if type(value) ~= "number" then return false end
    if value ~= value then return false end -- NaN check
    if min and value < min then return false end
    if max and value > max then return false end
    return true
end

function Validate.instance(value, className)
    if typeof(value) ~= "Instance" then return false end
    if className and not value:IsA(className) then return false end
    return true
end

function Validate.string(value, maxLength)
    if type(value) ~= "string" then return false end
    if maxLength and #value > maxLength then return false end
    return true
end

return Validate
lua
-- Usage
local Validate = require(script.Parent.Modules.Validate)

remote.OnServerEvent:Connect(function(player, amount, targetPart)
    if not Validate.number(amount, 1, 100) then return end
    if not Validate.instance(targetPart, "BasePart") then return end
    -- safe to proceed
end)

lua
-- ServerScriptService/Modules/Validate.lua
local Validate = {}

function Validate.number(value, min, max)
    if type(value) ~= "number" then return false end
    if value ~= value then return false end -- NaN校验
    if min and value < min then return false end
    if max and value > max then return false end
    return true
end

function Validate.instance(value, className)
    if typeof(value) ~= "Instance" then return false end
    if className and not value:IsA(className) then return false end
    return true
end

function Validate.string(value, maxLength)
    if type(value) ~= "string" then return false end
    if maxLength and #value > maxLength then return false end
    return true
end

return Validate
lua
-- 使用示例
local Validate = require(script.Parent.Modules.Validate)

remote.OnServerEvent:Connect(function(player, amount, targetPart)
    if not Validate.number(amount, 1, 100) then return end
    if not Validate.instance(targetPart, "BasePart") then return end
    -- 后续逻辑安全可执行
end)

Speed / Anti-Cheat Detection

速度/反作弊检测

lua
local SPEED_LIMIT = 32
local violations = {}

task.spawn(function()
    while true do
        task.wait(2)
        for _, player in ipairs(game.Players:GetPlayers()) do
            local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
            if root and root.AssemblyLinearVelocity.Magnitude > SPEED_LIMIT then
                violations[player] = (violations[player] or 0) + 1
                if violations[player] >= 3 then
                    player:Kick("Cheating detected.")
                end
            else
                violations[player] = math.max(0, (violations[player] or 0) - 1)
            end
        end
    end
end)

lua
local SPEED_LIMIT = 32
local violations = {}

task.spawn(function()
    while true do
        task.wait(2)
        for _, player in ipairs(game.Players:GetPlayers()) do
            local root = player.Character and player.Character:FindFirstChild("HumanoidRootPart")
            if root and root.AssemblyLinearVelocity.Magnitude > SPEED_LIMIT then
                violations[player] = (violations[player] or 0) + 1
                if violations[player] >= 3 then
                    player:Kick("检测到作弊行为")
                end
            else
                violations[player] = math.max(0, (violations[player] or 0) - 1)
            end
        end
    end
end)

ModuleScript Placement

ModuleScript放置规范

ServerScriptService/
  Modules/
    DamageCalculator.lua   -- server-only, never exposed to client
    EconomyManager.lua     -- server-only

ReplicatedStorage/
  Remotes/                 -- RemoteEvent/RemoteFunction instances only
  SharedModules/           -- non-sensitive utilities only
Never put currency, damage, or DataStore logic in
ReplicatedStorage
modules — clients can
require()
them.

ServerScriptService/
  Modules/
    DamageCalculator.lua   -- 仅服务端可用,绝对不暴露给客户端
    EconomyManager.lua     -- 仅服务端可用

ReplicatedStorage/
  Remotes/                 -- 仅存放RemoteEvent/RemoteFunction实例
  SharedModules/           -- 仅存放非敏感工具代码
永远不要将货币、伤害或DataStore逻辑放在
ReplicatedStorage
的模块中——客户端可以
require()
这些模块。

Common Mistakes

常见错误

MistakeWhy It's ExploitableFix
FireServer(damage)
with server trusting it
Client sends any valueServer calculates damage from its own tool data
Currency in LocalScript variableClient can modify memoryServer-owned only
Client-side distance check before firingCheck is bypassableServer re-checks after receiving event
No cooldown on RemoteEvent handlersSpam = infinite resourcesPer-player cooldown on server
Trusting
WalkSpeed
set by client
Client sets arbitrarily highServer owns and caps WalkSpeed
Sensitive logic in ReplicatedStorage moduleClients can require itMove to ServerScriptService
错误做法可被利用的原因修复方案
服务端直接信任
FireServer(damage)
传递的数值
客户端可以发送任意数值服务端根据自身存储的工具数据计算伤害
货币存储在LocalScript变量中客户端可以修改内存数据货币数据仅由服务端持有
发送请求前仅在客户端做距离检查检查逻辑可以被绕过服务端收到事件后重新做距离校验
RemoteEvent处理逻辑没有冷却限制恶意刷请求可以获取无限资源服务端按玩家维度设置冷却
信任客户端设置的
WalkSpeed
客户端可以设置任意高的移动速度移动速度由服务端持有并做上限限制
敏感逻辑放在ReplicatedStorage的模块中客户端可以require这些模块将敏感逻辑移动到ServerScriptService