roblox-security
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRoblox 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
安全与不安全模式对比
| Pattern | Insecure | Secure |
|---|---|---|
| Dealing damage | LocalScript sets | Server reduces health after validation |
| Awarding currency | LocalScript increments leaderstats | Server validates action, then increments |
| Leaderstats ownership | LocalScript owns the IntValue | Server creates and owns all leaderstats |
| Position changes | LocalScript teleports character | Server validates and moves character |
| Tool use | Client fires damage on hit | Server raycasts and applies damage |
| Cooldowns | Client tracks cooldown locally | Server tracks cooldown per player |
| 场景 | 不安全做法 | 安全做法 |
|---|---|---|
| 造成伤害 | LocalScript直接设置 | 服务端校验后再减少生命值 |
| 发放货币 | 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 Validatelua
-- 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 Validatelua
-- 使用示例
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 onlyNever put currency, damage, or DataStore logic in modules — clients can them.
ReplicatedStoragerequire()ServerScriptService/
Modules/
DamageCalculator.lua -- 仅服务端可用,绝对不暴露给客户端
EconomyManager.lua -- 仅服务端可用
ReplicatedStorage/
Remotes/ -- 仅存放RemoteEvent/RemoteFunction实例
SharedModules/ -- 仅存放非敏感工具代码永远不要将货币、伤害或DataStore逻辑放在的模块中——客户端可以这些模块。
ReplicatedStoragerequire()Common Mistakes
常见错误
| Mistake | Why It's Exploitable | Fix |
|---|---|---|
| Client sends any value | Server calculates damage from its own tool data |
| Currency in LocalScript variable | Client can modify memory | Server-owned only |
| Client-side distance check before firing | Check is bypassable | Server re-checks after receiving event |
| No cooldown on RemoteEvent handlers | Spam = infinite resources | Per-player cooldown on server |
Trusting | Client sets arbitrarily high | Server owns and caps WalkSpeed |
| Sensitive logic in ReplicatedStorage module | Clients can require it | Move to ServerScriptService |
| 错误做法 | 可被利用的原因 | 修复方案 |
|---|---|---|
服务端直接信任 | 客户端可以发送任意数值 | 服务端根据自身存储的工具数据计算伤害 |
| 货币存储在LocalScript变量中 | 客户端可以修改内存数据 | 货币数据仅由服务端持有 |
| 发送请求前仅在客户端做距离检查 | 检查逻辑可以被绕过 | 服务端收到事件后重新做距离校验 |
| RemoteEvent处理逻辑没有冷却限制 | 恶意刷请求可以获取无限资源 | 服务端按玩家维度设置冷却 |
信任客户端设置的 | 客户端可以设置任意高的移动速度 | 移动速度由服务端持有并做上限限制 |
| 敏感逻辑放在ReplicatedStorage的模块中 | 客户端可以require这些模块 | 将敏感逻辑移动到ServerScriptService |