roblox-security

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Roblox Security & Anti-Exploit

Roblox 安全与反漏洞

Use this skill when designing security systems, auditing existing code for vulnerabilities, or hardening a game against common exploit vectors.
在设计安全系统、审计现有代码漏洞或加固游戏以抵御常见攻击向量时使用此技能。

Core Principle

核心原则

The client is compromised. Always. Exploiters run arbitrary Luau on the client via injection tools. Every LocalScript, every ReplicatedStorage module, every client-side value is readable and writable by attackers. The server is the only source of truth.
客户端始终处于被攻陷状态。攻击者可通过注入工具在客户端运行任意Luau代码。所有LocalScript、ReplicatedStorage模块以及客户端侧的值都能被攻击者读取和修改。服务器是唯一的可信数据源。

Exploit Vectors & Mitigations

攻击向量与缓解措施

Movement Exploits

移动漏洞

AttackHow it worksMitigation
Speed hackClient moves faster than WalkSpeed allowsServer-side velocity check per Heartbeat
TeleportClient sets HumanoidRootPart.CFrame directlyServer tracks last valid position, reject jumps > threshold
Fly hackClient removes gravity/collisionServer checks if player is grounded or has valid flight state
NoclipClient passes through CanCollide partsServer raycasts between positions to detect wall passes
luau
-- Server-side movement validation (basic)
local MAX_SPEED = 16 * 1.5 -- WalkSpeed + tolerance
local MAX_TELEPORT = 50 -- studs per check

local lastPositions: {[Player]: Vector3} = {}

game:GetService("RunService").Heartbeat:Connect(function()
    for _, player in Players:GetPlayers() do
        local char = player.Character
        if not char then continue end
        local root = char:FindFirstChild("HumanoidRootPart")
        if not root then continue end

        local pos = root.Position
        local last = lastPositions[player]
        if last then
            local dist = (pos - last).Magnitude
            if dist > MAX_TELEPORT then
                -- Snap back to last valid position
                root.CFrame = CFrame.new(last)
            end
        end
        lastPositions[player] = pos
    end
end)
攻击类型攻击原理缓解方案
加速外挂客户端移动速度超过WalkSpeed限制服务器端每帧(Heartbeat)检查移动速度
瞬移外挂客户端直接设置HumanoidRootPart.CFrame服务器追踪玩家上一个有效位置,拒绝超过阈值的位置跳跃
飞行外挂客户端移除重力/碰撞检测服务器检查玩家是否处于地面或拥有合法飞行状态
穿墙外挂客户端穿过CanCollide属性开启的部件服务器在两个位置之间进行射线检测,判断是否穿墙
luau
-- Server-side movement validation (basic)
local MAX_SPEED = 16 * 1.5 -- WalkSpeed + tolerance
local MAX_TELEPORT = 50 -- studs per check

local lastPositions: {[Player]: Vector3} = {}

game:GetService("RunService").Heartbeat:Connect(function()
    for _, player in Players:GetPlayers() do
        local char = player.Character
        if not char then continue end
        local root = char:FindFirstChild("HumanoidRootPart")
        if not root then continue end

        local pos = root.Position
        local last = lastPositions[player]
        if last then
            local dist = (pos - last).Magnitude
            if dist > MAX_TELEPORT then
                -- Snap back to last valid position
                root.CFrame = CFrame.new(last)
            end
        end
        lastPositions[player] = pos
    end
end)

Remote Exploits

远程攻击

AttackHow it worksMitigation
Remote spamFire remotes at extreme ratesPer-player rate limiter
Argument spoofingSend wrong types/valuesValidate every argument type and range
Remote sniffingRead remote names to reverse-engineer APIDoesn't matter if validation is solid
Replay attackRe-fire a valid remote callIdempotency checks, transaction IDs
luau
-- Rate limiter pattern
local rateLimits: {[Player]: {[string]: number}} = {}
local RATE_LIMIT = 10 -- calls per second

local function checkRate(player: Player, remoteName: string): boolean
    local now = os.clock()
    local playerLimits = rateLimits[player]
    if not playerLimits then
        playerLimits = {}
        rateLimits[player] = playerLimits
    end

    local lastCall = playerLimits[remoteName] or 0
    if now - lastCall < 1 / RATE_LIMIT then
        return false -- rate limited
    end
    playerLimits[remoteName] = now
    return true
end

-- Argument validation pattern
local function validatePurchase(player: Player, itemId: unknown, quantity: unknown): boolean
    if typeof(itemId) ~= "string" then return false end
    if typeof(quantity) ~= "number" then return false end
    if quantity ~= math.floor(quantity) then return false end -- must be integer
    if quantity < 1 or quantity > 99 then return false end -- sane range
    if not ITEM_CATALOG[itemId] then return false end -- item must exist
    return true
end
攻击类型攻击原理缓解方案
远程调用刷屏高频触发远程调用为每个玩家设置调用频率限制器
参数伪造发送错误类型/数值的参数验证每个参数的类型和取值范围
远程调用嗅探读取远程调用名称以逆向工程API只要验证机制足够完善,此攻击无需过度担忧
重放攻击重复触发合法的远程调用幂等性检查、使用唯一交易ID
luau
-- Rate limiter pattern
local rateLimits: {[Player]: {[string]: number}} = {}
local RATE_LIMIT = 10 -- calls per second

local function checkRate(player: Player, remoteName: string): boolean
    local now = os.clock()
    local playerLimits = rateLimits[player]
    if not playerLimits then
        playerLimits = {}
        rateLimits[player] = playerLimits
    end

    local lastCall = playerLimits[remoteName] or 0
    if now - lastCall < 1 / RATE_LIMIT then
        return false -- rate limited
    end
    playerLimits[remoteName] = now
    return true
end

-- Argument validation pattern
local function validatePurchase(player: Player, itemId: unknown, quantity: unknown): boolean
    if typeof(itemId) ~= "string" then return false end
    if typeof(quantity) ~= "number" then return false end
    if quantity ~= math.floor(quantity) then return false end -- must be integer
    if quantity < 1 or quantity > 99 then return false end -- sane range
    if not ITEM_CATALOG[itemId] then return false end -- item must exist
    return true
end

Economy Exploits

经济系统攻击

AttackHow it worksMitigation
Item duplicationRace condition in trade/saveSession locking, atomic operations
Negative purchaseSend negative quantity to gain itemsValidate quantity > 0 server-side
Transaction replayReplay a purchase remoteUnique transaction IDs, check if already processed
DataStore rollbackExploit save timing to duplicateSession locking with server JobId
攻击类型攻击原理缓解方案
物品复制交易/保存时的竞态条件会话锁定、原子操作
负数量购买发送负数量参数以获取物品服务器端验证数量必须大于0
交易重放重放购买远程调用使用唯一交易ID,检查是否已处理
DataStore回滚利用保存时机漏洞复制物品使用服务器JobId进行会话锁定

DataStore Exploits

DataStore攻击

AttackHow it worksMitigation
Save spamForce repeated saves to exhaust budgetServer-controlled save intervals only
Session hijackFake session to duplicate across serversSession lock with JobId verification
BindToClose skipExploit shutdown timingBindToClose with parallel saves + timeout
攻击类型攻击原理缓解方案
保存刷屏强制重复保存以耗尽配额仅由服务器控制保存间隔
会话劫持伪造会话以跨服务器复制数据结合JobId验证的会话锁定
跳过BindToClose利用关机时机漏洞BindToClose结合并行保存与超时机制

Security Audit Checklist

安全审计 checklist

Run through this for every game before publish:
CRITICAL (game-breaking if missing):
[ ] All game state is server-authoritative
[ ] All RemoteEvent handlers validate types of EVERY argument
[ ] All RemoteEvent handlers have rate limiting
[ ] DataStore operations use session locking
[ ] No client-side currency/inventory mutations
[ ] MarketplaceService purchases verified via ProcessReceipt
[ ] No sensitive logic in LocalScripts or ReplicatedStorage

HIGH (exploitable if missing):
[ ] Player movement is server-validated
[ ] BindToClose saves protected against data loss
[ ] Trading system uses atomic operations
[ ] No trusting client-reported values (damage, position, items)
[ ] RemoteFunction return values not trusted by server

MEDIUM (quality/fairness):
[ ] Cooldowns enforced server-side (not just client UI)
[ ] Leaderboard values computed server-side
[ ] Anti-AFK detection for reward systems
[ ] Chat filter applied (TextService:FilterStringAsync)
发布游戏前需逐一检查以下内容:
CRITICAL (缺失会导致游戏崩溃):
[ ] 所有游戏状态由服务器管控
[ ] 所有RemoteEvent处理器验证每个参数的类型
[ ] 所有RemoteEvent处理器设置频率限制
[ ] DataStore操作使用会话锁定
[ ] 客户端不修改货币/库存数据
[ ] MarketplaceService购买通过ProcessReceipt验证
[ ] 敏感逻辑不放在LocalScript或ReplicatedStorage中

HIGH (缺失会导致可被攻击):
[ ] 玩家移动由服务器验证
[ ] BindToClose保存机制防止数据丢失
[ ] 交易系统使用原子操作
[ ] 不信任客户端上报的值(伤害、位置、物品)
[ ] 服务器不信任RemoteFunction的返回值

MEDIUM (影响游戏质量/公平性):
[ ] 冷却时间由服务器端强制执行(而非仅客户端UI)
[ ] 排行榜数值由服务器计算
[ ] 奖励系统设置防挂机检测
[ ] 应用聊天过滤(TextService:FilterStringAsync)

Patterns

设计模式

Never Trust Client Values

永远不要信任客户端的值

luau
-- BAD: Client tells server how much damage to deal
DamageRemote.OnServerEvent:Connect(function(player, target, damage)
    target.Humanoid:TakeDamage(damage) -- exploiter sends 999999
end)

-- GOOD: Server computes damage from game state
AttackRemote.OnServerEvent:Connect(function(player, targetId)
    local weapon = getEquippedWeapon(player)
    if not weapon then return end
    local target = resolveTarget(targetId)
    if not target then return end
    if not isInRange(player, target, weapon.Range) then return end
    if not checkCooldown(player, weapon) then return end

    local damage = weapon.BaseDamage * getDamageMultiplier(player)
    target.Humanoid:TakeDamage(damage)
end)
luau
-- BAD: Client tells server how much damage to deal
DamageRemote.OnServerEvent:Connect(function(player, target, damage)
    target.Humanoid:TakeDamage(damage) -- exploiter sends 999999
end)

-- GOOD: Server computes damage from game state
AttackRemote.OnServerEvent:Connect(function(player, targetId)
    local weapon = getEquippedWeapon(player)
    if not weapon then return end
    local target = resolveTarget(targetId)
    if not target then return end
    if not isInRange(player, target, weapon.Range) then return end
    if not checkCooldown(player, weapon) then return end

    local damage = weapon.BaseDamage * getDamageMultiplier(player)
    target.Humanoid:TakeDamage(damage)
end)

Session Locking

会话锁定

luau
-- Prevent data duplication across servers
local function acquireSessionLock(player: Player, data): boolean
    local lockKey = "SessionLock_" .. player.UserId
    local currentLock = data:GetMetadata().SessionLock

    if currentLock and currentLock ~= game.JobId then
        -- Another server owns this data
        -- Either wait for expiry or kick
        return false
    end

    data:SetMetadata({SessionLock = game.JobId})
    return true
end
luau
-- Prevent data duplication across servers
local function acquireSessionLock(player: Player, data): boolean
    local lockKey = "SessionLock_" .. player.UserId
    local currentLock = data:GetMetadata().SessionLock

    if currentLock and currentLock ~= game.JobId then
        -- Another server owns this data
        -- Either wait for expiry or kick
        return false
    end

    data:SetMetadata({SessionLock = game.JobId})
    return true
end

Sanity Checks (Defense in Depth)

合理性检查(纵深防御)

Even with server authority, add sanity checks for values that should be bounded:
luau
-- Clamp values that should never exceed known bounds
local function sanitizePlayerStats(stats)
    stats.Level = math.clamp(stats.Level, 1, MAX_LEVEL)
    stats.Gold = math.max(stats.Gold, 0) -- never negative
    stats.Health = math.clamp(stats.Health, 0, stats.MaxHealth)
    return stats
end
即使有服务器权限管控,仍需对有边界限制的值添加合理性检查:
luau
-- Clamp values that should never exceed known bounds
local function sanitizePlayerStats(stats)
    stats.Level = math.clamp(stats.Level, 1, MAX_LEVEL)
    stats.Gold = math.max(stats.Gold, 0) -- never negative
    stats.Health = math.clamp(stats.Health, 0, stats.MaxHealth)
    return stats
end

What NOT to Do

禁忌操作

  • Don't obfuscate client code — it doesn't stop exploiters and makes debugging harder
  • Don't use _G for security state — it's globally readable and writable
  • Don't kick without logging — you need data to distinguish false positives from real exploits
  • Don't over-validate movement — too strict = legitimate players get false-flagged on lag spikes. Use tolerance.
  • Don't rely on client-side anti-cheat — exploiters disable it first
  • 不要混淆客户端代码——这无法阻止攻击者,还会增加调试难度
  • 不要用_G存储安全状态——它是全局可读可写的
  • 不要不记录就踢出玩家——你需要数据区分误判与真实攻击
  • 不要过度验证移动——过于严格会导致合法玩家在网络波动时被误判,需设置容错范围
  • 不要依赖客户端反作弊——攻击者会首先禁用它