roblox-security について
Roblox 開発者向けに、サーバー側の検証と不正対策を扱うスキルです。重要なゲーム処理をサーバーに置き、クライアントを信頼することで生じる脆弱性を減らします。
主な機能
- 01サーバー主導のゲームロジック
- 02RemoteEvent 引数の検証
- 03プレイヤーごとのクールダウンとレート制限
- 04速度とステータスの妥当性確認
- 05ModuleScript の安全な配置
活用シーン
- 01ダメージ、通貨、購入処理の検証
- 02リモート操作やアビリティ使用の確認
- 03Roblox ゲームのクライアントとサーバーの境界確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ sentinelcore/ roblox-skills --skill roblox-securityインストール後、Claude Code で/roblox-securityを入力して読み込まれたか確認します。
詳細
Roblox セキュリティ: エクスプロイト対策とサーバーサイド検証
基本原則
クライアントを決して信頼しないでください。 すべての LocalScript はプレイヤーのマシン上で実行されるため、改変される可能性があります。ダメージ、通貨、ステータス、位置の変更など、すべての権威あるロジックはサーバー上に置く必要があります。
FilteringEnabled は現代の Roblox では常に有効です。サーバーが明示的に適用しない限り、クライアント側の変更はサーバーや他のクライアントに複製されません。
安全なパターンと安全でないパターン
| パターン | 安全でない | 安全 |
|---|---|---|
| ダメージの処理 | LocalScript が Humanoid.Health を設定する | サーバーが検証後にヘルスを減少させる |
| 通貨の付与 | LocalScript が leaderstats をインクリメントする | サーバーがアクションを検証してからインクリメントする |
| Leaderstats の所有権 | LocalScript が IntValue を所有する | サーバーがすべての leaderstats を作成して所有する |
| 位置の変更 | LocalScript がキャラクターをテレポートさせる | サーバーが検証してキャラクターを移動させる |
| ツールの使用 | クライアントがヒット時にダメージを発火する | サーバーがレイキャストしてダメージを適用する |
| クールダウン | クライアントがクールダウンをローカルで管理する | サーバーがプレイヤーごとにクールダウンを管理する |
安全な Leaderstats のセットアップ
-- 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)
サーバーサイドのサニティチェック
距離チェック
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)
クールダウン検証
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)
ステータス境界チェック
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)
レート制限
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)
引数検証ユーティリティ
-- 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
-- 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)
速度 / チート対策検出
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)
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
通貨、ダメージ、DataStore のロジックを ReplicatedStorage のモジュールに置かないでください。クライアントが require() できてしまいます。
よくある間違い
| 間違い | なぜエクスプロイト可能か | 修正 |
|---|---|---|
FireServer(damage) をサーバーが信頼する | クライアントが任意の値を送信できる | サーバーが自身のツールデータからダメージを計算する |
| 通貨を LocalScript の変数に置く | クライアントがメモリを変更できる | サーバー所有のみにする |
| 発火前のクライアント側距離チェック | チェックがバイパス可能 | サーバーがイベント受信後に再チェックする |
| RemoteEvent ハンドラにクールダウンがない | スパム = 無限のリソース | サーバーでプレイヤーごとのクールダウンを設定する |
クライアントが設定した WalkSpeed を信頼する | クライアントが任意の高い値を設定できる | サーバーが WalkSpeed を所有して上限を設ける |
| ReplicatedStorage モジュールに機密ロジックを置く | クライアントが require できる | ServerScriptService に移動する |
コメント
Markdown 対応