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.

warningLuaudit check: replaced-invoke-callbacks

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. Unlike OnServerEvent, 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 OnServerInvoke for each RemoteFunction in 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 OnServerInvoke to nil during 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.