Why the client shouldn't choose your HttpService requests
A remote that builds a request address from client input lets players call your API with the game's key. How to map client choices to fixed addresses.
What it looks like
A remote handler makes a web request whose address, path, query string, method or headers come from the client. The server adds its own key or token, so the client gets to call the service with the game's credentials.
remote.OnServerInvoke = function(player, path) return HttpService:GetAsync(baseUrl .. path, false, { Authorization = token }) end
How to detect it
- Find every
GetAsync,PostAsyncandRequestAsynccall and trace itsUrl,MethodandHeadersback to their source. Follow helper modules that wrap requests. - A finding is any part of the address before the query values, any whole query string, the method or any header that comes from a remote argument without being matched against a fixed list on the server.
- Type, length and character-pattern checks don't count. A string of the right length can still be any path, such as an admin endpoint on the same service.
- A single value from the client, such as a
UserId, is fine when it's checked as the type it should be, such as a whole number, and placed into a fixed address.
What an exploiter could do
They can make your server call any endpoint the key can reach: admin or delete endpoints on your own backend, other players' records, or, with an Open Cloud key, any of the Roblox APIs that key has permission for, such as data stores or bans. If the client can choose the whole address and the secret's domain isn't restricted, they can also have the key sent to an address they own.
How to fix it
- Let the client pick from a short list of actions. Map each one to a fixed address and method on the server.
- Check any value the client provides as the type it should be (a whole number, an id from a known list) before putting it into the address, and encode strings with
HttpService:UrlEncode. - Never take headers or methods from the client.
- Give the secret a specific domain in the secrets store, and give the key only the permissions the game needs.
- Add a per-player cooldown, since every call uses the server's request budget.
Vulnerable example
local HttpService = game:GetService("HttpService") local ReplicatedStorage = game:GetService("ReplicatedStorage") ReplicatedStorage.Remotes.GetClanInfo.OnServerInvoke = function(player: Player, query: unknown) if typeof(query) ~= "string" or #query > 100 then return nil end local found, token = pcall(HttpService.GetSecret, HttpService, "ClanToken") if not found then return nil end local ok, response = pcall(HttpService.RequestAsync, HttpService, { Url = `https://clans.example.com/api/{query}`, Method = "GET", Headers = { Authorization = token:AddPrefix("Bearer ") }, }) return if ok and response.Success then response.Body else nil end
Fixed example
local HttpService = game:GetService("HttpService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local cooldown = 2 local lastLookup: { [Player]: number } = {} ReplicatedStorage.Remotes.GetClanInfo.OnServerInvoke = function(player: Player, clanId: unknown) if typeof(clanId) ~= "number" or clanId ~= math.floor(clanId) or clanId < 1 or clanId > 2 ^ 31 then return nil end local now = os.clock() if now - (lastLookup[player] or -math.huge) < cooldown then return nil end lastLookup[player] = now local found, token = pcall(HttpService.GetSecret, HttpService, "ClanToken") if not found then return nil end local ok, response = pcall(HttpService.RequestAsync, HttpService, { Url = `https://clans.example.com/api/clans/{string.format("%d", clanId)}`, Method = "GET", Headers = { Authorization = token:AddPrefix("Bearer ") }, }) return if ok and response.Success then response.Body else nil end Players.PlayerRemoving:Connect(function(player) lastLookup[player] = nil end)
Common false positives
- Requests with no key or token to public, read-only data, where the response goes back only to the player who asked, give the client nothing it couldn't fetch itself. Check the rate limit still.
- A client choice that's only used to pick from a server-side table of addresses is an allowlist, not a client-built request.
- Values that only go into the request body as data, checked for type and length, aren't part of the address. Player text sent on to a webhook is Player text sent to webhooks with no limits.