1. Validate type and structure
Roblox warns that exploiters can send a table in place of an Instance, or send a string that is extremely long to slow down any server code that scales with string length. Check that an argument is the type you expect, and check that an Instance is where you expect it. The docs' shop example rejects an item unless it is a descendant of the real ItemData folder:
Code: Select all
if typeof(item) ~= "Instance" or not item:IsDescendantOf(itemDataFolder) then
return false, "Invalid item provided"
endA purchase quantity should be greater than zero, and an item ID should match a real item. The docs point out that NaN has the type "number" but fails every comparison, so range checks like "offeredGold < 0 or offeredGold > 1000000" do nothing against it. Their fix is math.isfinite():
Code: Select all
if not math.isfinite(offeredGold) then
return false, "Invalid number provided"
endAsk whether this player is allowed to make this request right now. The docs use these examples: is the player close enough to the shop, do they have the key for the door, is their character alive.
4. Rate limit on the server
Client-side cooldowns are fine for feel, but the docs say never to rely on them alone. They describe a token bucket: each player has a bucket that holds a set number of tokens, every action spends one, and the bucket refills at a steady rate. That allows short bursts and blocks sustained spam.
5. Do not forget the non-remote triggers
ProximityPrompt, ClickDetector and DragDetector events can be triggered by an exploiter from any distance. For a ProximityPrompt, Roblox says only the Triggered event has a server-side distance check, so other events such as PromptButtonHoldBegan and TriggerEnded can be sent by any client. Check the object's state, the player's state and a rate limit in your own handler.
6. Be a gatekeeper, not a relay
If a remote makes the server call FireAllClients() or FireClient() with data the client supplied, validate and rate limit before broadcasting. Also avoid remotes that let a client name an arbitrary path or instance for the server to delete or modify.
7. Purchases are server-side only
For game passes and developer products, grant items inside the ProcessReceipt callback after checking the receipt. Roblox says not to trust a client-side signal such as PromptProductPurchaseFinished, because it can be spoofed.
If you have a remote you are unsure about, post the server handler below (not the whole game) and people here can look at what it trusts from the client.
Source
[1] Securing the client-server boundary, Roblox Creator Hub