How to filter player text in Roblox with TextService
Pet names, signs and plot names typed by players must be filtered before others see them. How to use FilterStringAsync and the right TextFilterResult method on the server.
What it looks like
A player types something, such as a pet name, a sign, a plot name or a team name, and the client sends it to the server. The server puts it somewhere other players can see it without passing it through TextService first.
Luau
remote.OnServerEvent:Connect(function(player, text) sign.SurfaceGui.TextLabel.Text = text end)
How to detect it
- Find handlers that receive a string the player typed and then show it to anyone: a
Textproperty on aSurfaceGui,BillboardGuiorScreenGui, an attribute or value object that clients display, aFireAllClientsorFireClientcall, or a name set on an Instance that other players can see. - Look for
TextService:FilterStringAsync(text, player.UserId)before the text is shown, followed byGetNonChatStringForBroadcastAsync()when everyone in the server can see it. GetNonChatStringForUserAsync(userId)filters for one player only. Text filtered that way and then shown to everyone is a finding.- Text fetched from the web or generated from random letters needs the same filtering before it's shown. So does text loaded from a data store, even if it was filtered when it was first written.
- Messages sent through
TextChatServiceare filtered by Roblox. Custom text outside the chat isn't. - Filtering on the client doesn't count. The client can skip it and fire the remote with anything.
What an exploiter could do
They can put slurs, abuse or personal information, such as phone numbers, in front of every player in the server. Roblox requires text you didn't write to be filtered, and its documentation says a game found not to filter text is removed until filtering is added. So one player can get the whole game taken down.
How to fix it
- Check the argument is a string, with a sensible length limit, before filtering.
- On the server, call
FilterStringAsyncwith the UserId of the player who wrote the text, thenGetNonChatStringForBroadcastAsync()for text everyone can see, orGetNonChatStringForUserAsync()for text only one player will see. - Both calls yield and can fail, so wrap them in
pcall. If filtering fails, don't show the text at all. Roblox's documentation says not to retryFilterStringAsyncyourself, as it already retries. FilterStringAsyncthrows if the author isn't in the server. Filter saved text while its author is in the server, such as when their plot loads, and don't show it if filtering fails or the author has left.- Filter once per submission, not on every keystroke.
Vulnerable example
Vulnerable
local ReplicatedStorage = game:GetService("ReplicatedStorage") local Workspace = game:GetService("Workspace") local board = Workspace:WaitForChild("NoticeBoard") local label = board:WaitForChild("SurfaceGui"):WaitForChild("TextLabel") :: TextLabel local maxLength = 80 ReplicatedStorage.Remotes.PostNotice.OnServerEvent:Connect(function(player: Player, text: unknown) if typeof(text) ~= "string" or #text == 0 or #text > maxLength then return end label.Text = player.DisplayName .. ": " .. text end)
Fixed example
Fixed
local ReplicatedStorage = game:GetService("ReplicatedStorage") local TextService = game:GetService("TextService") local Workspace = game:GetService("Workspace") local board = Workspace:WaitForChild("NoticeBoard") local label = board:WaitForChild("SurfaceGui"):WaitForChild("TextLabel") :: TextLabel local maxLength = 80 local function filterForEveryone(text: string, author: Player): string? local ok, result = pcall(function() return TextService:FilterStringAsync(text, author.UserId):GetNonChatStringForBroadcastAsync() end) if ok then return result end return nil end ReplicatedStorage.Remotes.PostNotice.OnServerEvent:Connect(function(player: Player, text: unknown) if typeof(text) ~= "string" or #text == 0 or #text > maxLength then return end local filtered = filterForEveryone(text, player) if not filtered then return end label.Text = player.DisplayName .. ": " .. filtered end)
Common false positives
- Text the server chose itself, such as a name picked from an allowlist or a number, doesn't need filtering.
- Text only ever shown back to the player who typed it, and never sent anywhere else, isn't something other players can see. Showing it to anyone else needs filtering.
- Chat sent through
TextChatServiceis filtered by Roblox. - Filtering done in a helper module counts. Read it before reporting, and check which
TextFilterResultmethod it uses.