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.

warningLuaudit check: trusted-chat-metadata

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.

Luau
TextChatService.OnIncomingMessage = function(message)
	if message.Metadata == "staff" then
		...
	end
end

How to detect it

  • Search for Metadata in OnIncomingMessage and OnBubbleAdded callbacks, ShouldDeliverCallback, MessageReceived handlers and TextChatCommand code.
  • Roblox's reference says TextChannel:SendAsync, which only runs on the client, takes a metadata argument that fills TextChatMessage.Metadata. A client can send any metadata up to 200 characters, so it says nothing trustworthy about the sender.
  • Metadata from DisplaySystemMessage is 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 check message.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 the Player.
  • Only treat metadata as meaningful for messages with no TextSource, such as the game's own DisplaySystemMessage calls.
  • Never let metadata change what the server grants or allows.

Vulnerable example

Vulnerable
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

Fixed
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 DisplaySystemMessage and checked with TextSource being nil, 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.