mcpField
R
roblox-security

roblox-security

github.com/el4cteoセキュリティ56537 回インストール2 週間前に更新
リポジトリ

roblox-security について

roblox-securityは、Roblox開発者向けのゲームセキュリティスキルです。サーバー側の設計を軸に、検証、レート制限、移動・戦闘チェック、経済機能の保護、検知方法を扱います。

主な機能

  1. 01サーバー側の検証と権限管理
  2. 02クライアント起点の処理のレート制限
  3. 03プロンプトや接触イベントの安全な処理
  4. 04移動と戦闘の検証
  5. 05経済機能と購入処理の保護
  6. 06不正検知と対応の指針

活用シーン

  1. 01RemoteEventとRemoteFunctionのハンドラーの確認
  2. 02戦闘・移動システムの強化
  3. 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)を編集し、ネットワーク所有権を持つものを何でも 動かせます。クライアント側のアンチチートは、せいぜい足止めになる程度です。セキュリティはサーバーの設計によって確保します。

基本ルール

  1. サーバーが決定します。 クライアントが送信するのは意図(「ここを狙って攻撃ボタンを押しました」)であり、結果 (「50 ダメージを与えました」「コインを 100 枚ください」)ではありません。
  2. すべての OnServerEvent/OnServerInvoke のすべての引数を検証します。型、構造、範囲、有限性を確認します。
  3. コンテキストを検証します。プレイヤーは生存しているか、範囲内か、クールダウン中ではないか、アイテムを所有しているか、行動が許可されているかを確認します。
  4. クライアントが呼び出せるあらゆるもの(リモート、プロンプト、タッチ、クリック)にレート制限を設けます。
  5. 秘密情報はサーバー側に保持します。サーバーコードは ServerScriptService/ServerStorage に置き、API キーには HttpService:GetSecret() を使用します。未公開コンテンツを本番用プレースに含めてはいけません。
  6. 即時キックよりも目立たない緩和策(破棄、上限設定、状態の修正)を優先し、シグナルが繰り返された場合に段階的に対処します。

引数検証ツールキット

--!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 の抑制が可能です。対策は効果の高い順に次のとおりです。

  1. サーバー権限(Workspace の AuthorityMode = Server。正式リリース済み、オプトイン方式):サーバーがクライアント予測とロールバックを用いて移動をシミュレートし、移動に関するエクスプロイトを排除します。 roblox-physics を参照してください。
  2. サーバー側で移動をチェックします。インターバルごとの水平(XZ)変位を、最大速度に遅延許容値を加えた値と比較します。正当なテレポートは例外扱いにします(自分でプレイヤーを移動させる際にサーバー側でフラグを設定します)。キックする代わりに、最後に有効だった位置まで引き戻します。
  3. ゲームプレイ上重要な、固定されていないパーツはサーバー所有のままにします。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。

コメント