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.

warningLuaudit check: unescaped-request-text

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.

Luau
HttpService:GetAsync(apiUrl .. "/search?game=" .. gameKey .. "&q=" .. text)

How to detect it

  • Find every GetAsync, PostAsync and RequestAsync call and trace each piece of its Url and Body back 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.format or an interpolated string needs HttpService: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 from HttpService:JSONEncode on a table.
  • Form bodies sent as ApplicationUrlEncoded need each value encoded with UrlEncode too.
  • 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

Vulnerable
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

Fixed
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 JobId or 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.