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.

criticalLuaudit check: wrong-player-in-handler

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 OnServerEvent connection and every OnServerInvoke assignment 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, character or profile variable captured from the outer function is a finding.
  • OnServerEvent runs 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.
  • OnServerInvoke holds 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 Player and 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

  • PlayerAdded handlers that only set things up for that player, such as creating leaderstats or loading data, without connecting remotes, are fine.
  • A remote connected inside PlayerAdded that checks the passed player against the captured one and is disconnected when the player leaves acts only for the caller.
  • FireClient(player, ...) calls inside PlayerAdded send to that player only. Sending isn't handling.