roblox-security について
roblox-securityは、Roblox開発者向けのゲームセキュリティスキルです。サーバー側の設計を軸に、検証、レート制限、移動・戦闘チェック、経済機能の保護、検知方法を扱います。
主な機能
- 01サーバー側の検証と権限管理
- 02クライアント起点の処理のレート制限
- 03プロンプトや接触イベントの安全な処理
- 04移動と戦闘の検証
- 05経済機能と購入処理の保護
- 06不正検知と対応の指針
活用シーン
- 01RemoteEventとRemoteFunctionのハンドラーの確認
- 02戦闘・移動システムの強化
- 03取引とゲーム内購入の保護
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ el4cteo/ roblox-skills --skill roblox-securityインストール後、Claude Code で/roblox-securityを入力して読み込まれたか確認します。
同梱ファイルreferences/checklist.md
詳細
Roblox のセキュリティ
エクスプロイターはクライアントを完全に制御できます。任意の Luau を実行し、複製されたすべてのスクリプトを
(逆コンパイルして)読み取り、任意のリモートを任意の引数・頻度で呼び出し、ローカルの任意のプロパティ(WalkSpeed、
プロンプトの Enabled、MaxActivationDistance)を編集し、ネットワーク所有権を持つものを何でも
動かせます。クライアント側のアンチチートは、せいぜい足止めになる程度です。セキュリティはサーバーの設計によって確保します。
基本ルール
- サーバーが決定します。 クライアントが送信するのは意図(「ここを狙って攻撃ボタンを押しました」)であり、結果 (「50 ダメージを与えました」「コインを 100 枚ください」)ではありません。
- すべての
OnServerEvent/OnServerInvokeのすべての引数を検証します。型、構造、範囲、有限性を確認します。 - コンテキストを検証します。プレイヤーは生存しているか、範囲内か、クールダウン中ではないか、アイテムを所有しているか、行動が許可されているかを確認します。
- クライアントが呼び出せるあらゆるもの(リモート、プロンプト、タッチ、クリック)にレート制限を設けます。
- 秘密情報はサーバー側に保持します。サーバーコードは
ServerScriptService/ServerStorageに置き、API キーにはHttpService:GetSecret()を使用します。未公開コンテンツを本番用プレースに含めてはいけません。 - 即時キックよりも目立たない緩和策(破棄、上限設定、状態の修正)を優先し、シグナルが繰り返された場合に段階的に対処します。
引数検証ツールキット
--!strict
-- ServerScriptService/Server/Validate.luau
local Validate = {}
function Validate.number(value: unknown, min: number, max: number): number?
if typeof(value) ~= "number" or not math.isfinite(value) then
return nil -- rejects NaN and ±inf (NaN fails every comparison, so range checks alone don't catch it)
end
if value < min or value > max then
return nil
end
return value
end
function Validate.integer(value: unknown, min: number, max: number): number?
local n = Validate.number(value, min, max)
return if n and n == math.floor(n) then n else nil
end
function Validate.string(value: unknown, maxLength: number): string?
if typeof(value) ~= "string" or #value > maxLength or utf8.len(value) == nil then
return nil -- also rejects invalid UTF-8 (which DataStores refuse)
end
return value
end
function Validate.vector3(value: unknown, maxMagnitude: number): Vector3?
if typeof(value) ~= "Vector3" then
return nil
end
if not (math.isfinite(value.X) and math.isfinite(value.Y) and math.isfinite(value.Z)) then
return nil
end
return if value.Magnitude <= maxMagnitude then value else nil
end
-- Accept only a real instance of the expected class that lives under a trusted container.
-- Exploiters can send tables shaped like instances ({ ClassName = "Part", ... }).
function Validate.instanceIn(value: unknown, className: string, container: Instance): Instance?
if typeof(value) ~= "Instance" or not value:IsA(className) or not value:IsDescendantOf(container) then
return nil
end
return value
end
function Validate.enum<T>(value: unknown, allowed: { [T]: boolean }): T?
if allowed[value :: any] then
return value :: any
end
return nil
end
return Validate
playerの後にあるすべてのハンドラー引数はunknownとして扱い、これらのヘルパーで型を絞り込みます。- アイテムは サーバー側のテーブルで ID を使って検索します(
ITEMS[itemId])。クライアントデータや、クライアントが参照できる複製されたNumberValueから価格やステータスを読み取ってはいけません。 - テーブルのサイズに上限を設け、クライアントのテーブルを防御的に反復処理します(キーが NaN のような値、入れ子構造、巨大なサイズである可能性があります)。
レート制限(トークンバケット)
--!strict
local Players = game:GetService("Players")
type Bucket = { tokens: number, last: number }
local function newLimiter(capacity: number, windowSeconds: number)
local refillRate = capacity / windowSeconds
local buckets: { [Player]: Bucket } = {}
Players.PlayerRemoving:Connect(function(player)
buckets[player] = nil
end)
return function(player: Player): boolean
local now = os.clock()
local bucket = buckets[player]
if not bucket then
bucket = { tokens = capacity, last = now }
buckets[player] = bucket
else
bucket.tokens = math.min(capacity, bucket.tokens + (now - bucket.last) * refillRate)
bucket.last = now
end
if bucket.tokens >= 1 then
bucket.tokens -= 1
return true
end
return false
end
end
local allowAttack = newLimiter(5, 1) -- bursts of 5, refills 5/second
print(allowAttack)
特に、DataStore/Badge/HTTP 呼び出し(それぞれに固有の上限があります)、大きなモデルの複製、テレポート、通貨の付与、すべてのクライアントへのブロードキャストなど、負荷が高い操作や悪用されやすい操作にはレート制限を設けます。
クライアントが起動するインスタンスは安全ではありません
| オブジェクト | エクスプロイターができること | サーバーで必要な対応 |
|---|---|---|
ProximityPrompt | 無効中でもイベントを発火できます。距離/視線判定/HoldDuration をローカルで変更できます。距離チェックが行われるのは Triggered のみです。 | 有効状態、キャラクターからの距離、生存状態、クールダウンを再確認します。 |
ClickDetector | 無効中でも、どの距離からでも発火できます。サーバー側のチェックは一切ありません。 | 上記と同じです。 |
DragDetector | 継承されたクリックイベントはチェックされません。ドラッグで考慮されるのは Enabled と PermissionPolicy のみです。 | ドラッグの結果をサーバー側で検証します。 |
クライアント所有のパーツに対する Touched | タッチを偽装したり、抑制したりできます。 | サーバー側の距離/重なり判定クエリで検証します。 |
これらのオブジェクトが、クライアントが所有できる固定されていないパーツにある場合、エクスプロイターはパーツを自分のところまで引き寄せられます。固定するか、サーバー所有にするために part:SetNetworkOwner(nil) を使用します。
ネットワーク所有権と移動
クライアントは自分のキャラクターの物理演算を所有しているため、テレポート、飛行、ノークリップ、スピードハック、他のプレイヤーを吹き飛ばす(NaN/巨大な速度)、Touched の抑制が可能です。対策は効果の高い順に次のとおりです。
- サーバー権限(Workspace の
AuthorityMode = Server。正式リリース済み、オプトイン方式):サーバーがクライアント予測とロールバックを用いて移動をシミュレートし、移動に関するエクスプロイトを排除します。roblox-physicsを参照してください。 - サーバー側で移動をチェックします。インターバルごとの水平(XZ)変位を、最大速度に遅延許容値を加えた値と比較します。正当なテレポートは例外扱いにします(自分でプレイヤーを移動させる際にサーバー側でフラグを設定します)。キックする代わりに、最後に有効だった位置まで引き戻します。
- ゲームプレイ上重要な、固定されていないパーツはサーバー所有のままにします。
part:SetNetworkOwner(nil)を使用します。
戦闘の検証(ヒット報告)
クライアントは起点、方向/対象パーツ、タイムスタンプを送信します。サーバーは次の点を確認します。
- 起点がキャラクターのサーバー側の位置に近いこと(許容範囲は
player:GetNetworkPing()に応じて調整します)。 - 対象が生存している敵であり、武器の射程内にいること。報告された着弾点が対象のサーバー側の位置に近いこと。
- 静的ジオメトリに対するサーバーのレイキャストで、起点と対象の間に壁がないこと。
- 発射レート、弾薬、リロード/スプリント状態をサーバー側で追跡します。
経済、取引、購入
- 通貨、インベントリ、価格はサーバー上にのみ存在します。クライアントはリクエストし、サーバーが残高を 確認して変更をアトミックに適用します。
- 取引: 双方を完全に検証してから、双方をコミットし、双方を保存します。いずれかが失敗した場合はすべてを ロールバックします。「取引してから保存前に退出する」ことによる複製に注意してください。取引中はアイテムを ロックし、直後に保存します。
- Robux 購入: 付与するのは
MarketplaceService.ProcessReceipt(開発者製品)内、またはサーバー側の 所有権チェック(パス)後のみです。クライアントからのPromptProductPurchaseFinishedを決して信用しないでください。 詳細はroblox-monetizationを参照してください。
機密性
- レプリケートされる
LocalScript、クライアントのScript、またはModuleScriptは、無効化されている 場合や一度も require されていない場合でも逆コンパイルされる可能性があります。サーバー専用の分岐を 共有モジュールに混在させないでください。 - レプリケートされるもの(モデル、UI、属性)はすべてデータマイニングされる可能性があります。未公開の コンテンツは本番環境に隠すのではなく、別のプライベートユニバースに保管してください。
- クライアントはユニバース内の任意のプレイスにテレポートできます。Creator Dashboard で Access Control for Places → Secure within universe only を設定し、制限付きプレイスでは参加時に 資格を検証してください(デフォルトは拒否)。
- API キーをスクリプトや属性に決して入れないでください。体験内の Secrets ストア
(
HttpService:GetSecret)を使用してください。
検出と結果
検証の上にサーバー側のヒューリスティクスを重ねます。可能な限り最速の完了時間、リソース獲得の速度、 人間離れして規則的なアクションのケイデンス、正規のクライアントが発火しないハニーポットのリモートなどです。 疑わしさスコアを蓄積し、複数のシグナルに基づいて対応します。段階は、サイレントログ → 静かな緩和 → 機能の制限 → キック/BAN です。目に見える結果を遅延させて、エクスプロイターがチェックを二分探索できない ようにしてください。
--!strict
local Players = game:GetService("Players")
local function banForExploiting(player: Player, evidence: string)
local ok, err = pcall(function()
Players:BanAsync({
UserIds = { player.UserId },
Duration = 7 * 24 * 60 * 60, -- seconds; -1 = permanent
DisplayReason = "Exploiting detected.", -- shown to the user (max 400 chars, filtered)
PrivateReason = evidence, -- visible to you only
ExcludeAltAccounts = false, -- also ban detected alt accounts
ApplyToUniverse = true,
})
end)
if not ok then
warn("BanAsync failed:", err)
player:Kick("Exploiting detected.")
end
end
return banForExploiting
Ban API を使用するには Players.BanningEnabled を有効にしてください。可逆的で期間限定の BAN を
推奨します(エスカレーションのために Players:GetBanHistoryAsync を確認してください)。検出ルールの
バージョン管理を行い、誤検知率を監視してください。
セキュリティレビューのチェックリストとよくある脆弱なコードパターン: references/checklist.md。
関連スキル
roblox-networking、roblox-physics(所有権、サーバー権限)、roblox-data-stores、
roblox-monetization、roblox-code-review。