Why a RemoteFunction should have one OnServerInvoke
Only the last OnServerInvoke assignment runs, so a leftover script can silently replace your checks. How to find and remove duplicates.
What it looks like
Two server scripts assign OnServerInvoke on the same RemoteFunction. Only the last assignment runs, so the checks in the other one silently stop applying.
Luau
remotes.GetShopPrice.OnServerInvoke = function(player, item) ... end
How to detect it
- Search every server script and module for
OnServerInvoke =and group the assignments by remote. - Roblox's remote events guide says that if you define multiple callbacks for the same
RemoteFunction, only the last one runs. UnlikeOnServerEvent, which can have many connections, a new callback replaces the old. - Script start-up order isn't something to rely on. Treat every assignment except one as dead, and check that the one that might win does every check the others do.
- A second assignment in a free model or a script with an unrelated name, replacing a callback that had checks with one that doesn't, may be deliberate. Report it under Powers given to accounts outside the team as well.
- The same applies on the client to
OnClientInvoke, though that only affects the player's own client.
What an exploiter could do
They call the remote and get the behaviour of whichever callback won: a version without a staff check, without a price check, or a leftover from an old feature. Which one wins can change between servers, so the bug may come and go.
How to fix it
- Assign
OnServerInvokefor eachRemoteFunctionin exactly one place, and route different requests through one callback. - Remove old callbacks when a feature is rewritten.
Vulnerable example
Vulnerable
local ReplicatedStorage = game:GetService("ReplicatedStorage") local prices: { [string]: number } = { Sword = 100, Shield = 150, } ReplicatedStorage.Remotes.GetPrice.OnServerInvoke = function(player: Player, item: unknown) return if typeof(item) == "string" then prices[item] else nil end ReplicatedStorage.Remotes.GetPrice.OnServerInvoke = function(player: Player, item: unknown) return 0 end
Fixed example
Fixed
local ReplicatedStorage = game:GetService("ReplicatedStorage") local prices: { [string]: number } = { Sword = 100, Shield = 150, } ReplicatedStorage.Remotes.GetPrice.OnServerInvoke = function(player: Player, item: unknown) return if typeof(item) == "string" then prices[item] else nil end
Common false positives
- Assigning
OnServerInvoketonilduring shutdown or cleanup, with no other callback left behind, is fine. - Different
RemoteFunctions that share a name in different folders are separate remotes. Check the full path before reporting. - One script that sets the callback again in a loop or on respawn with the same function is untidy but not this finding.