How to keep roles and answers hidden from exploiters
Anything sent to a client or left in ReplicatedStorage can be read. How to send each player only what they're allowed to know.
What it looks like
The game depends on something players aren't meant to know yet: who the murderer is, where the treasure is buried, which door is safe, the answer to a quiz question. The server sends it to every client, or puts it somewhere that replicates, and trusts the client's UI to hide it.
remotes.RolesAssigned:FireAllClients(roles)
How to detect it
- Find hidden state the game keeps from players: roles, answers, locations, upcoming loot or anything a round's outcome turns on.
- Follow where it goes. Treat these as sent to the client:
FireAllClients,FireClientto players who shouldn't know it, attributes or value objects on anything inWorkspace,ReplicatedStorageor a player, and ModuleScripts or other Instances inReplicatedStorageorReplicatedFirst. - A client script that receives the full data and only shows part of it is still a finding. What reaches the client can be read on the client.
- Check what's sent at the start of a round, not only the reveal at the end.
What an exploiter could do
Anything that reaches their client can be read, and Roblox's own security guidance says exploiters can decompile any replicated LocalScript or ModuleScript. They can see every role, dig straight to the treasure or answer every question correctly. It doesn't change saved data on its own, but it decides who wins and ruins the round for everyone else.
How to fix it
- Keep hidden state in server-only places: script variables,
ServerStorageorServerScriptService. - Send each player only what they're allowed to know, with
FireClient. A murderer can be told they're the murderer without anyone else being told. - Reveal answers and roles when they stop being secret, such as at the end of the round.
- Check guesses on the server. The client sends its guess and the server says whether it was right.
- For hints such as hot or cold, work the hint out on the server and send only the hint.
Vulnerable example
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local remotes = ReplicatedStorage.Remotes local function startRound() local players = Players:GetPlayers() if #players < 3 then return end local murderer = players[math.random(#players)] local roles: { [string]: string } = {} for _, player in players do roles[player.Name] = if player == murderer then "Murderer" else "Innocent" end remotes.RolesAssigned:FireAllClients(roles) end return startRound
Fixed example
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local remotes = ReplicatedStorage.Remotes local function startRound() local players = Players:GetPlayers() if #players < 3 then return end local murderer = players[math.random(#players)] for _, player in players do remotes.RoleAssigned:FireClient(player, if player == murderer then "Murderer" else "Innocent") end end return startRound
Common false positives
- Information that's meant to be public, such as prices, level requirements or a scoreboard, is fine to replicate. Trusting it when the client sends it back is a different problem.
- Data sent with
FireClientonly to the players who are allowed to know it is fine. - A secret revealed after it stops mattering, such as roles at the end of the round, is fine.
- Server-only containers,
ServerStorageandServerScriptService, don't replicate.