Treat a network event as an untrusted request
RegisterNetEvent enables network transport; it does not authenticate a payload or make a client honest. The server must decide what the caller may do, validate the requested action, and derive sensitive values from server-owned state.
Use AddEventHandler for events that should remain within one context. Do not register an internal reward or administrative handler as a network event merely because it is convenient. For syntax and routing, see events.
Complete example: request a documentation topic
This resource accepts only two topic names, checks a permission, and acknowledges the request. It does not create a persistent support ticket or expose arbitrary text to an administrator. Create resources/[local]/doc_help/ with the following files.
fxmanifest.lua
fx_version 'cerulean'
game 'gta5'
client_script 'client.lua'
server_script 'server.lua'server.lua
local topics = {
controls = 'Open settings to inspect your FiveM key bindings.',
connection = 'Record the exact error and contact the server staff.'
}
local lastRequest = {}
RegisterNetEvent('doc_help:request', function(topic)
local playerId = source
if type(playerId) ~= 'number' or playerId <= 0 then return end
-- Rate-limit attempts, including invalid or unauthorized requests.
local now = os.time()
if lastRequest[playerId] and now - lastRequest[playerId] < 3 then return end
lastRequest[playerId] = now
if type(topic) ~= 'string' or #topic > 24 or not topics[topic] then return end
if not IsPlayerAceAllowed(playerId, 'doc.help') then
TriggerClientEvent('doc_help:reply', playerId, 'This action is not permitted.')
return
end
print(('[doc_help] Session %s requested topic %s'):format(playerId, topic))
TriggerClientEvent('doc_help:reply', playerId, topics[topic])
end)
AddEventHandler('playerDropped', function()
lastRequest[source] = nil
end)client.lua
RegisterCommand('dochelp', function(_, args)
TriggerServerEvent('doc_help:request', args[1] or 'controls')
end, false)
RegisterNetEvent('doc_help:reply', function(message)
if source ~= 65535 then return end -- Expect a server-origin event.
if type(message) ~= 'string' or #message > 200 then return end
print(('[Docs] %s'):format(message))
end)The client-side origin check filters unexpected local dispatch; a modified client can still change its own display. The security boundary is the server’s validation, not that client check, the event name, or an obscured script.
Grant only the permission being demonstrated
Replace the identifier below with your test account’s real server-verified license identifier. In server.cfg:
add_ace group.docreader doc.help allow
add_principal identifier.license:REPLACE_WITH_YOUR_LICENSE group.docreader
ensure doc_helpUse refresh first if the running server has not discovered the new resource. In the client’s F8 console run dochelp controls. The permitted player sees the fixed help message. An unpermitted player sees a refusal. The custom doc.help object is separate from a command.* ACE; see permissions.
Abuse tests for your development server
| Input or condition | Expected result |
|---|---|
dochelp controls with permission | Fixed controls message |
| Same request without permission | No privileged action; refusal |
| Unknown topic, number, table, or oversized string | Ignored before business logic |
| Repeated requests within three seconds | Extra attempts ignored |
| Another player requests a topic | Separate per-player rate limit |
| Player disconnects | Rate-limit entry removed |
To exercise non-string payloads, temporarily call TriggerServerEvent('doc_help:request', {}) from your own development client script. Do not perform abuse tests against someone else’s server. This small in-process limiter demonstrates the boundary; it is not a general anti-cheat, durable quota service, or protection against all network flooding.
Applying the pattern to money and inventory
For a purchase, accept an item identifier and intended action—not a client-chosen price, balance, owner, or reward. Resolve the player from server source; check current server-side inventory, permissions, and relevant world state. Use a transaction or idempotent operation so two concurrent requests cannot duplicate a grant. Revalidate after asynchronous work and confirm the original session is still connected.
State bags, NUI messages, hidden buttons, and client exports are not substitutes for those checks. Avoid automatic bans from one malformed payload; programming errors and version mismatches can produce bad requests too.
The official event-security guide documents these trust boundaries. Continue with connection-time checks, async source handling, and database access.