How network ownership lets clients move reward parts
An unanchored part near a player is simulated by their client. How to stop balls, crates and carts deciding rewards from client physics.
What it looks like
An unanchored part decides a reward: a ball that scores when it reaches a goal, a crate that pays when it lands in a drop zone, a cart that finishes a race. The server reads where the part is, or listens to its Touched event, but a player's client is simulating that part's physics.
goal.Touched:Connect(function(hit) if hit == ball then awardGoal(lastKicker) end end)
How to detect it
- Find rewards, scores and progress that depend on an unanchored part that isn't a character: its
PositionorCFrame, its velocity,TouchedorTouchEnded,GetPartsInPartor a region check around it. - Check who owns the part. The server always owns anchored parts. An unanchored part near a player is handed to that player's client automatically unless the server has called
SetNetworkOwner(nil)on it. A call toSetNetworkOwner(player)hands it over on purpose. SetNetworkOwner(nil)has to be called again after the part is anchored and unanchored, since unanchoring resets ownership to automatic. A laterSetNetworkOwnershipAuto()call undoes it too.- Parts welded to a player's character are simulated by that player's client too.
- Characters themselves are always owned by their player. Rewards from a character touching something are covered by Rewards paid on touch with no distance check in the anti-exploit category.
What an exploiter could do
Roblox's documentation says it can't verify physics on a part a client owns: the client can teleport the part, push it through walls or make it fly, and can fire Touched for it even when the server never saw a touch. They can put the ball in the goal from the halfway line or drop every crate in the delivery zone the moment they pick it up.
How to fix it
- Call
SetNetworkOwner(nil)on parts that decide rewards, once they're inWorkspaceand unanchored, so the server simulates them. Roblox notes this can make physics feel less smooth for the player near the part, so keep it for parts that matter. - A touch between a server-owned part and a character can still be reported by that character's client, so check the character is really close before giving it credit.
- If the part has to stay client-owned for feel, don't reward from its position. Track the action on the server instead: who picked it up and when, where their character is, and whether the time taken was possible.
- Server ownership stops the client moving the part directly, but characters can still push it, and a client controls its own character. Server ownership raises the bar, but still check the play was plausible, such as the distance and time since the last real touch, and see Rewards paid on touch with no distance check.
- Anchored parts moved by the server, for example with
TweenServiceorPivotTo, are always server-owned.
Vulnerable example
local Players = game:GetService("Players") local ServerStorage = game:GetService("ServerStorage") local Workspace = game:GetService("Workspace") local PlayerData = require(script.Parent.PlayerData) local pitch = Workspace:WaitForChild("Pitch") local goal = pitch:WaitForChild("Goal") :: BasePart local centre = pitch:WaitForChild("Centre") :: BasePart local reward = 25 local lastKicker: Player? = nil local ball = (ServerStorage:WaitForChild("Ball") :: BasePart):Clone() ball.Parent = pitch ball.Touched:Connect(function(hit) local character = hit.Parent local player = if character and character:IsA("Model") then Players:GetPlayerFromCharacter(character) else nil if player then lastKicker = player end end) goal.Touched:Connect(function(hit) if hit ~= ball or not lastKicker then return end local profile = PlayerData.get(lastKicker) if profile then profile.Coins += reward end ball.CFrame = centre.CFrame ball.AssemblyLinearVelocity = Vector3.zero lastKicker = nil end)
Fixed example
local Players = game:GetService("Players") local ServerStorage = game:GetService("ServerStorage") local Workspace = game:GetService("Workspace") local PlayerData = require(script.Parent.PlayerData) local pitch = Workspace:WaitForChild("Pitch") local goal = pitch:WaitForChild("Goal") :: BasePart local centre = pitch:WaitForChild("Centre") :: BasePart local reward = 25 local reach = 8 local lastKicker: Player? = nil local ball = (ServerStorage:WaitForChild("Ball") :: BasePart):Clone() ball.Anchored = false ball.Parent = pitch ball:SetNetworkOwner(nil) ball.Touched:Connect(function(hit) local character = hit.Parent local player = if character and character:IsA("Model") then Players:GetPlayerFromCharacter(character) else nil local root = player and character and character:FindFirstChild("HumanoidRootPart") if player and root and root:IsA("BasePart") and (root.Position - ball.Position).Magnitude <= reach then lastKicker = player end end) goal.Touched:Connect(function(hit) if hit ~= ball or not lastKicker then return end local profile = PlayerData.get(lastKicker) if profile then profile.Coins += reward end ball.CFrame = centre.CFrame ball.AssemblyLinearVelocity = Vector3.zero lastKicker = nil end)
Common false positives
- Anchored parts are always owned by the server.
- Unanchored parts that give nothing, such as debris or props players can knock over, don't matter.
- A part the server has called
SetNetworkOwner(nil)on, and doesn't anchor and unanchor afterwards, is simulated by the server. - Vehicles and tools handed to a player for driving or holding are fine as long as nothing is paid out from where they end up.