How to send player text safely in HttpService requests
Joining player text into a URL or JSON string lets it add parameters or fields. How to use UrlEncode and JSONEncode instead.
What it looks like
The server sends player text, such as a search term, a pet name, a report or a username typed into a box, to a web service by joining it into a URL or a JSON string by hand. Characters like &, =, #, / or " in the text change the request.
HttpService:GetAsync(apiUrl .. "/search?game=" .. gameKey .. "&q=" .. text)
How to detect it
- Find every
GetAsync,PostAsyncandRequestAsynccall and trace each piece of itsUrlandBodyback to where it came from. - Text from a player, directly from a remote or from saved data they wrote, joined into a URL with
..,string.formator an interpolated string needsHttpService:UrlEncode. Without it,&and=add query parameters,#cuts off the rest, and/or..can change the path. - A JSON body built by joining strings, such as
'{"name":"' .. name .. '"}', lets a"in the text close the string and add fields. Bodies should come fromHttpService:JSONEncodeon a table. - Form bodies sent as
ApplicationUrlEncodedneed each value encoded withUrlEncodetoo. - A value checked as a whole number, or matched against a fixed list, can't change the request. Letters-only and similar pattern checks can be enough for one value, but say so explicitly in the finding's reasoning.
- If the client chooses the endpoint itself, that's Web requests built from what the client sends. This check is about text placed inside an otherwise fixed request.
What an exploiter could do
They send text that rewrites the request the server makes with its own key. Extra parameters can override ones the server set later in the address, such as a game or player id, or switch on options like admin=true if the service accepts them. Extra JSON fields can change what the service stores, such as the score or rank on a record. What's possible depends on the service, but the request no longer says what the code meant.
How to fix it
- Build bodies as Luau tables and send them through
HttpService:JSONEncode. - Pass every piece of player text in a URL through
HttpService:UrlEncode, and put values only in the query, not the path. - Put the values the server controls, such as ids and keys, where text can't reach them, such as headers, and check lengths before sending.
Vulnerable example
local HttpService = game:GetService("HttpService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local apiUrl = "https://api.example.com/v1/guilds" local cooldown = 10 local lastCreate: { [Player]: number } = {} ReplicatedStorage.Remotes.CreateGuild.OnServerEvent:Connect(function(player: Player, name: unknown) if typeof(name) ~= "string" or #name < 3 or #name > 20 then return end local now = os.clock() if now - (lastCreate[player] or -math.huge) < cooldown then return end lastCreate[player] = now local body = `\{"name":"{name}","owner":{player.UserId},"members":1}` local ok, err = pcall(HttpService.PostAsync, HttpService, apiUrl, body, Enum.HttpContentType.ApplicationJson) if not ok then warn(`Guild create failed: {err}`) end end) Players.PlayerRemoving:Connect(function(player) lastCreate[player] = nil end)
Fixed example
local HttpService = game:GetService("HttpService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local apiUrl = "https://api.example.com/v1/guilds" local cooldown = 10 local lastCreate: { [Player]: number } = {} ReplicatedStorage.Remotes.CreateGuild.OnServerEvent:Connect(function(player: Player, name: unknown) if typeof(name) ~= "string" or #name < 3 or #name > 20 then return end local now = os.clock() if now - (lastCreate[player] or -math.huge) < cooldown then return end lastCreate[player] = now local body = HttpService:JSONEncode({ name = name, owner = player.UserId, members = 1, }) local ok, err = pcall(HttpService.PostAsync, HttpService, apiUrl, body, Enum.HttpContentType.ApplicationJson) if not ok then warn(`Guild create failed: {err}`) end end) Players.PlayerRemoving:Connect(function(player) lastCreate[player] = nil end)
Common false positives
- Ids and numbers the server checked as whole numbers can't carry extra characters. Joining them into a URL is fine.
- Text the players never typed, such as a server's
JobIdor a fixed item name from a server table, can't be used to change the request. - Names shown to other players still need filtering under Player text shown to others without filtering if the service sends them back to be shown. That's a separate finding.