How often players should be able to change a sign

Roblox requires at least a minute between changes to text other players can see. How to rate limit signs and boards on the server.

warningLuaudit check: public-text-rate-limits

What it looks like

Players can write text that others see: a sign on their plot, a note on a bulletin board, a shop banner or a message on a whiteboard. The server filters it and has a cooldown, but the cooldown is shorter than a minute, or there isn't one.

Luau
local cooldown = 5

remotes.SetSign.OnServerEvent:Connect(function(player, text)
	if os.clock() - (lastEdit[player] or 0) < cooldown then return end
	...
end)

How to detect it

  • Find every place a player's text is shown to other players outside chat: signs, boards, notes, banners, plot or shop names, and any SurfaceGui or BillboardGui text the server sets from a player's input.
  • Roblox's chat guidelines say games with editable text that users can see between each other, such as signs or bulletin boards, must add rate limits of at least one minute. Its rate-limited text input example uses one post per player per minute.
  • Read the cooldown value and how it's applied. It must be per player, measured on the server, and at least 60 seconds between changes.
  • Check that the time is recorded only when the text is actually shown, and that a failed filter doesn't reset it in a way that lets text through faster.
  • Roblox's filtering guide says not to filter text per character as it's typed. A remote fired from a TextBox text-change event usually means the text is shown, and the limit tested, on every keystroke.

What an exploiter could do

They turn a sign or board into a chat window outside TextChatService, holding a conversation with other players that skips their chat settings and can't be reported. Changing it every few seconds also lets them flash filtered-but-unwanted messages at everyone nearby.

How to fix it

Vulnerable example

Vulnerable
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TextService = game:GetService("TextService")

local board = workspace:WaitForChild("NoticeBoard"):WaitForChild("SurfaceGui"):WaitForChild("Note") :: TextLabel
local cooldown = 3
local lastPost: { [Player]: number } = {}

ReplicatedStorage.Remotes.PostNote.OnServerEvent:Connect(function(player: Player, text: unknown)
	if typeof(text) ~= "string" or #text > 100 then
		return
	end
	if os.clock() - (lastPost[player] or 0) < cooldown then
		return
	end
	local ok, filtered = pcall(function()
		return TextService:FilterStringAsync(text, player.UserId):GetNonChatStringForBroadcastAsync()
	end)
	if ok then
		lastPost[player] = os.clock()
		board.Text = filtered
	end
end)

Players.PlayerRemoving:Connect(function(player)
	lastPost[player] = nil
end)

Fixed example

Fixed
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TextService = game:GetService("TextService")

local board = workspace:WaitForChild("NoticeBoard"):WaitForChild("SurfaceGui"):WaitForChild("Note") :: TextLabel
local cooldown = 60
local lastPost: { [Player]: number } = {}

ReplicatedStorage.Remotes.PostNote.OnServerEvent:Connect(function(player: Player, text: unknown)
	if typeof(text) ~= "string" or #text > 100 then
		return
	end
	if os.clock() - (lastPost[player] or 0) < cooldown then
		return
	end
	local ok, filtered = pcall(function()
		return TextService:FilterStringAsync(text, player.UserId):GetNonChatStringForBroadcastAsync()
	end)
	if ok then
		lastPost[player] = os.clock()
		board.Text = filtered
	end
end)

Players.PlayerRemoving:Connect(function(player)
	lastPost[player] = nil
end)

Common false positives

  • Names for a player's own things, such as a pet's name, are listed in Roblox's chat guidelines as text that isn't chat. They still need filtering, but a short cooldown isn't this finding.
  • Text only the writer can see, such as a private note, isn't shared between users.
  • Text the game writes itself, such as a round timer or a top-player sign, isn't player text.
  • Missing filtering is Player text shown to others without filtering, reported separately.