Why chat tags shouldn't come from message metadata
Any client can set a chat message's metadata, so staff tags based on it can be faked. How to decide tags from the sender instead.
What it looks like
The game styles or acts on chat messages using TextChatMessage.Metadata: a staff tag, a system-message colour, a VIP badge, or a command. For messages players type, the metadata is whatever the sender's client passed to SendAsync.
TextChatService.OnIncomingMessage = function(message) if message.Metadata == "staff" then ... end end
How to detect it
- Search for
MetadatainOnIncomingMessageandOnBubbleAddedcallbacks,ShouldDeliverCallback,MessageReceivedhandlers andTextChatCommandcode. - Roblox's reference says
TextChannel:SendAsync, which only runs on the client, takes ametadataargument that fillsTextChatMessage.Metadata. A client can send any metadata up to 200 characters, so it says nothing trustworthy about the sender. - Metadata from
DisplaySystemMessageis set by the local client for its own display, and can be trusted for that. Check whether the callback can tell the two apart. Roblox's chat examples checkmessage.TextSource, which holds the sender of a player's message. - Look for styling that makes a player's message look like staff, the game or Roblox: prefixes, colours, icons or a system-message layout.
- Report any server logic that grants anything, runs a command or skips a check because of metadata, as well as display-only cases.
What an exploiter could do
They send chat messages with the staff or system metadata, so their message shows with the staff tag or in the game's system style. Players trust it and follow fake instructions, such as "staff are giving away items, type your password" or "go to this game for free coins". Where the server reads metadata, they get whatever it grants.
How to fix it
- Decide tags and styles from
message.TextSource.UserId, compared with something the server controls, such as an attribute the server sets on thePlayer. - Only treat metadata as meaningful for messages with no
TextSource, such as the game's ownDisplaySystemMessagecalls. - Never let metadata change what the server grants or allows.
Vulnerable example
local TextChatService = game:GetService("TextChatService") TextChatService.OnIncomingMessage = function(message: TextChatMessage): TextChatMessageProperties? local properties = Instance.new("TextChatMessageProperties") if string.find(message.Metadata, "Staff") then properties.PrefixText = `<font color="#ff4040">[Staff]</font> {message.PrefixText}` elseif string.find(message.Metadata, "Announcement") then properties.Text = `<font color="#ffd040">{message.Text}</font>` end return properties end
Fixed example
local Players = game:GetService("Players") local TextChatService = game:GetService("TextChatService") TextChatService.OnIncomingMessage = function(message: TextChatMessage): TextChatMessageProperties? local properties = Instance.new("TextChatMessageProperties") local source = message.TextSource if source then local sender = Players:GetPlayerByUserId(source.UserId) if sender and sender:GetAttribute("Staff") == true then properties.PrefixText = `<font color="#ff4040">[Staff]</font> {message.PrefixText}` end elseif string.find(message.Metadata, "Announcement") then properties.Text = `<font color="#ffd040">{message.Text}</font>` end return properties end
The Staff attribute is set by a server script from its own staff list.
Common false positives
- Metadata used only to style the local player's own system messages, from
DisplaySystemMessageand checked withTextSourcebeingnil, is fine. - Cosmetic metadata that can't look like staff or the game, such as a chosen chat colour, is low risk. Mention it only if it could mislead players.