How to secure RedM server events: every TriggerServerEvent is attacker input
How to secure RedM server events: why every TriggerServerEvent is attacker input, the validation rules to apply, and what an anticheat adds on top.
If you want to secure RedM server events, start from one fact: the player controls the client, so the player controls every argument of every TriggerServerEvent. The money and items that vanish from roleplay servers rarely leave through a clever memory hack. They leave through a handler that trusted a number it was given.
This guide is for resource developers and server owners who review scripts. The examples are framework-neutral Lua. The same rules apply to any VORP, RSG or custom resource, and they are what makes a typical redm trigger exploit fail instead of paying out.
Why a client-triggered event is attacker-controlled input
A server event is a public door. Your client script calls TriggerServerEvent("shop:sell", item, count, price), and the server runs the handler. Nothing in that call proves the request came from your script. A mod menu or an injected resource can call the same event, with the same name, and put whatever it likes in the arguments.
Hiding the event name does not help. Client-side Lua ships to every player, and event names show up in it. Treat the name as public and the arguments as hostile, the way a web developer treats a form field.
That is the whole vorp exploit class in one sentence: a handler that applies what the client says. The rest of this guide is how to stop doing that.
A vulnerable handler, and the fixed one
Here is the shape we see most often. The client sends the item, the count and the price, and the server pays.
RegisterNetEvent('shop:sell', function(item, count, price)
local src = source
-- takes the client's word for everything
removeItem(src, item, count)
addMoney(src, count * price)
end)Every line is a bug. The caller picks the price, so one call with a huge price mints money. A negative count turns the removal into a gift. An item the player does not own may never be checked. The handler also never asks whether the player is standing at a shop.
The fixed version keeps the prices on the server, validates types and ranges, checks the player against the world, and refuses quietly when anything is off. removeItem, addMoney and getItemCount stand for your framework's own functions.
local PRICES = { apple = 2, bread = 3 } -- authority lives here
local SHOP = vector3(-324.0, 803.0, 117.0) -- example coordinates
local lastCall = {}
local function refuse(src, why)
print(('[shop:sell] refused src=%s reason=%s'):format(src, why))
end
RegisterNetEvent('shop:sell', function(item, count)
local src = source -- never trust an id from the client
local now = GetGameTimer()
if lastCall[src] and now - lastCall[src] < 1000 then
return refuse(src, 'rate')
end
lastCall[src] = now
if type(item) ~= 'string' or type(count) ~= 'number'
or count % 1 ~= 0 or count < 1 or count > 50 then
return refuse(src, 'bad_args')
end
local price = PRICES[item]
if not price then return refuse(src, 'unknown_item') end
local ped = GetPlayerPed(src)
if #(GetEntityCoords(ped) - SHOP) > 5.0 then
return refuse(src, 'too_far')
end
if getItemCount(src, item) < count then
return refuse(src, 'not_owned')
end
removeItem(src, item, count)
addMoney(src, count * price) -- the server computes the reward
end)
AddEventHandler('playerDropped', function() lastCall[source] = nil end)The client now sends two values, and the server treats both as suggestions. Each rule in that handler is worth its own paragraph.
The rules that secure RedM server events
1. Identify the player with source, and only source
Inside a server event handler, source is the player who triggered it, and it is set by the platform, not by the client. Capture it into a local at the top (local src = source) because it can change across a yield. Never accept a "player id" or "target id" argument to decide who gets paid or who gets hurt.
2. Never accept amounts, prices, item names or targets as truth
If the client has to say what it wants, look it up. Prices, rewards and item lists belong in a server-side table. If an argument is a target (another player, an entity, a coordinate), check that it exists and that the caller is allowed to act on it. The safest events have almost no arguments at all.
3. Validate types and ranges
Lua will happily pass a table where you expected a string, or -1, or a float, or a number so large that arithmetic misbehaves. Check type(), require integers where you mean integers, and set a hard upper bound that matches what the feature can legitimately do. Reject, do not clamp: a clamped request still tells you nothing about who sent it.
4. Check distance, job and state on the server
A legitimate sale happens at a shop, a legitimate craft at a bench, a legitimate arrest by a lawman. Verify that from server data: the player's position (with OneSync, server natives such as GetPlayerPed and GetEntityCoords work), their job from your framework, their current state from your own tables. The client UI hiding a button is not a check.
5. Let the server compute the reward
The strongest pattern is also the simplest: the client says "I finished", the server decides what that was worth. For longer activities, start the activity on the server and record when. When the client reports completion, check that enough time passed and that a session exists, then pay a server-side amount.
6. Rate-limit every event that moves value
A handler that is correct once can still be called two hundred times a second. Keep a per-player timestamp or counter, and drop what exceeds what a human could do. Clean the table on playerDropped, as in the example. Replays and request floods also stress the server, so a cheap early return matters.
7. Do not expose admin events to clients
An event named admin:giveMoney that checks nothing is a gift. If staff tools must use events, verify the caller on the server every time, with the framework's permission system or ACE, not with a client-side flag. Better still, use commands registered on the server, or events that are only ever triggered server-to-server.
8. Log refusals
The refuse function above is not decoration. A refusal is a signal: either a bug in your own client script, or someone probing. A line per refusal, with the player and the reason, turns your logs into a list of leads. One caution: a refusal alone proves nothing, so review before you ban.
Where this goes wrong in practice
Inherited scripts. A paid or leaked resource that trusts its arguments stays vulnerable until someone reads it. Grep your server resources for
RegisterNetEventandRegisterServerEventand read every handler that touches money, items, jobs or inventory."Temporary" debug events. The event added for testing and never removed.
Trusting a flag set by the client.
isAdmin,isOnDutyand similar values sent as arguments are just more attacker input.Fixing one handler. If three resources pay out of the same inventory function, the weakest caller decides your security.
When the damage is already done, the pattern in the data looks like a dupe or a sudden wealth outlier. Our write-up on the RedM money dupe exploit covers how to spot that from the economy side.
What an anticheat adds on top of redm event protection
Careful handlers remove the vulnerability. An anticheat watches for the people who try anyway, and for the handlers you have not reviewed yet. Baobab AC does this on the server side, and its catalogue has four modules that matter here. See all features for the full list.
Anti Blacklist Trigger prevents cheat menus from calling hidden server events to do things like unauthorized job changes or bypassing economy rules.
Anti Spam Trigger blocks request flooding aimed at overwhelming the server backend.
Anti Economy Trigger Exploit prevents cheaters from injecting fake hidden commands to give themselves money, gold or valuable items.
Anti Native Injection hooks the natives that create entities and change player state, so a spawn or state change that no server resource asked for is caught as it happens.
On the Pro and Network plans, Economy Forensics adds a read-only audit of your game database, cross-checked with the playtime only the anticheat knows: money supply over time, wealth outliers, item dupes, impossible stacks. Every result is a lead for a human to review, never an automatic sanction. Detection is framework-agnostic, and VORP Core is the reference for the economy modules. The VORP anticheat guide goes into that setup, and how a mod menu gets caught explains the detection side of the same story.
You can look at what this output looks like in the demo dashboard, and compare plans on the pricing page.
What an anticheat cannot do
Three more limits are worth stating. Detection is probabilistic, so the shipped posture is observe: a doubtful detection costs a line in a list, not a kick. Economy Forensics tells you something looks wrong, not why, and a human has to read it. And an anticheat sees the client and the traffic, not your business logic, so only you know that a 400 dollar sale at that shop is impossible.
The two layers fit together. Server-side authority makes the exploit impossible. The anticheat catches the attempt, and the audit shows what slipped through before you fixed it. If you want the broader argument for why trust has to sit on the server, read server-side vs client-side anticheat.
A checklist to secure RedM server events today
- 1
List every
RegisterNetEventandRegisterServerEventin your resources. - 2
For each one that touches money, items, jobs or inventory, read the handler.
- 3
Replace every client-supplied price, reward and player id with a server lookup or
source. - 4
Add type and range checks, then a position or state check.
- 5
Add a per-player rate limit and a refusal log.
- 6
Remove debug and admin events that skip checks.
- 7
Only then turn on anticheat modules to cover what review missed. Start in observe, read the alerts, and tighten from there. How a RedM anticheat works explains the pipeline.
A trusting handler is cheap to write and expensive to find. An hour reading your server events now costs less than one night of a stolen economy.