Why remote handlers inside PlayerAdded act for the wrong player
Connecting OnServerEvent inside PlayerAdded and using the outer player lets one client act for everyone. How to connect remotes once and use the player argument.
What it looks like
A remote handler is connected inside PlayerAdded, CharacterAdded or another per-player function, and acts for the player captured from that outer function instead of the player the remote passes in. Often the handler's own first parameter is missing or named _.
Luau
Players.PlayerAdded:Connect(function(player) remotes.Rebirth.OnServerEvent:Connect(function() rebirth(player) end) end)
How to detect it
- Find every
OnServerEventconnection and everyOnServerInvokeassignment made inside a function that runs once per player:PlayerAdded,CharacterAdded, a profile-loaded callback, or a setup function called with a player. - In each handler, check which player it acts for. It must be the first parameter the remote passes. A handler that uses a
player,characteror profile variable captured from the outer function is a finding. OnServerEventruns every connected handler for every call. With one connection per player, one call from any client runs once for each player who has joined, each acting for its captured player.OnServerInvokeholds one callback. Assigning it per player replaces it each time, so every caller is answered for whoever joined last.- A per-player connection is fine when it compares the passed player with the captured one and returns when they differ, and is disconnected when the player leaves. Without the disconnect it's also Connections and loops that pile up; report it once, here.
- Remotes inside tools that act for the tool's holder are Tool remotes any player can fire, not this check.
What an exploiter could do
One call from any client acts for other players. Depending on the remote, the exploiter can spend everyone's coins, rebirth or reset every player, sell their items, claim rewards in their names, or read their data. Each call also runs once per player in the server, so the cost grows with the player count.
How to fix it
- Connect each remote once, at the top level of a server script, and act only for the player the remote passes in.
- Keep per-player state in tables keyed by
Playerand look it up from that argument. - If a handler really must be connected per player, return when the passed player isn't the captured one, and disconnect it in
PlayerRemoving.
Vulnerable example
Vulnerable
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local PlayerData = require(script.Parent.PlayerData) local rebirthCost = 1000000 Players.PlayerAdded:Connect(function(player) ReplicatedStorage.Remotes.Rebirth.OnServerEvent:Connect(function() local profile = PlayerData.get(player) if not profile or profile.Coins < rebirthCost then return end profile.Coins = 0 profile.Level += 1 end) end)
Fixed example
Fixed
local ReplicatedStorage = game:GetService("ReplicatedStorage") local PlayerData = require(script.Parent.PlayerData) local rebirthCost = 1000000 ReplicatedStorage.Remotes.Rebirth.OnServerEvent:Connect(function(player: Player) local profile = PlayerData.get(player) if not profile or profile.Coins < rebirthCost then return end profile.Coins = 0 profile.Level += 1 end)
Common false positives
PlayerAddedhandlers that only set things up for that player, such as creating leaderstats or loading data, without connecting remotes, are fine.- A remote connected inside
PlayerAddedthat checks the passed player against the captured one and is disconnected when the player leaves acts only for the caller. FireClient(player, ...)calls insidePlayerAddedsend to that player only. Sending isn't handling.