Why the server shouldn't wait on InvokeClient
RemoteFunction:InvokeClient lets a client stall or break server code. How to replace it with two RemoteEvents and a timeout the server controls.
What it looks like
Server code calls RemoteFunction:InvokeClient and waits for the answer, often while holding a lock, a "busy" flag or a half-finished change.
Luau
local accepted = remotes.TradePrompt:InvokeClient(target, player)How to detect it
- Search server scripts and server-side modules for
InvokeClient. - For each call, look at what the server has already changed before the call and what it undoes after. Anything set before and cleared after is stuck if the call never returns or throws.
- Check whether the return value is trusted. It comes from the client and needs the same checks as any remote argument.
OnServerInvokeis the other direction, client asking server, and is not this check.
What an exploiter could do
The client decides when, and whether, InvokeClient returns. If it never answers, the server thread waits forever. If it throws an error, or the player leaves mid-call, InvokeClient errors on the server and the code after it never runs. Whatever was set before the call stays set: in a trade, both players can be locked out of trading until they rejoin. The answer, when it comes, can be any value.
How to fix it
- Replace
InvokeClientwith two RemoteEvents: the server fires a request withFireClient, and the client answers on a second remote. - Keep pending requests on the server with the time they were sent. Accept an answer only from the player it was sent to, only while it's pending and only before a timeout.
- Clear pending entries in
Players.PlayerRemoving. - If you must keep
InvokeClient, don't hold locks or half-made changes across it, wrap it inpcall, and validate what comes back.
Vulnerable example
Vulnerable
local ReplicatedStorage = game:GetService("ReplicatedStorage") local remotes = ReplicatedStorage.Remotes local trading: { [Player]: boolean } = {} remotes.RequestTrade.OnServerEvent:Connect(function(player: Player, target: unknown) if typeof(target) ~= "Instance" or not target:IsA("Player") or target == player then return end if trading[player] or trading[target] then return end trading[player] = true trading[target] = true local accepted = remotes.TradePrompt:InvokeClient(target, player) if accepted == true then remotes.TradeStarted:FireClient(player, target) remotes.TradeStarted:FireClient(target, player) end trading[player] = nil trading[target] = nil end)
Fixed example
Fixed
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") type Request = { from: Player, sentAt: number, } local remotes = ReplicatedStorage.Remotes local timeout = 30 local pending: { [Player]: Request } = {} local function activeRequest(player: Player): Request? local request = pending[player] if request and os.clock() - request.sentAt <= timeout and request.from.Parent == Players then return request end pending[player] = nil return nil end remotes.RequestTrade.OnServerEvent:Connect(function(player: Player, target: unknown) if typeof(target) ~= "Instance" or not target:IsA("Player") or target == player then return end if activeRequest(target) then return end pending[target] = { from = player, sentAt = os.clock() } remotes.TradeRequest:FireClient(target, player) end) remotes.AnswerTrade.OnServerEvent:Connect(function(player: Player, accepted: unknown) local request = activeRequest(player) if not request or typeof(accepted) ~= "boolean" then return end pending[player] = nil if accepted then remotes.TradeStarted:FireClient(request.from, player) remotes.TradeStarted:FireClient(player, request.from) end end) Players.PlayerRemoving:Connect(function(player) pending[player] = nil end)
Common false positives
OnServerInvokehandlers, where the client calls the server, don't wait on a client. Check them against the other remote checks instead.InvokeClientwrapped inpcall, run in a thread nothing else waits on (its owntask.spawn, or an event handler such asPlayerAdded), holding no locks or partial changes, and with its result validated, has dealt with the risk. Mention it in "What you did well".