Engineering
Keeping your database safe from SQL injection
Prevent SQL injection in FiveM resources: how string-built queries get exploited through events, oxmysql placeholders (? and named parameters), why table and column names must be whitelisted, LIKE searches, numeric validation, least-privilege database users, and auditing old scripts.
Overview
A single query built by gluing a player’s input into a string can let a cheater read your whole database, give themselves money, or drop tables. Old FiveM scripts are full of these. The fix has been available in every database library for years: placeholders.
What an injection looks like
RegisterNetEvent('garage:find', function(plate)
MySQL.query('SELECT * FROM owned_vehicles WHERE plate = "' .. plate .. '"')
end)A crafted plate value can close the string and append its own SQL. Any client can trigger the event with any text — see secure server events.
The fix: placeholders
RegisterNetEvent('garage:find', function(plate)
local src = source
if type(plate) ~= 'string' or #plate > 8 then return end
local rows = MySQL.query.await(
'SELECT plate, vehicle FROM owned_vehicles WHERE plate = ? AND owner = ?',
{ plate, GetPlayerIdentifierByType(src, 'license') }
)
end)
-- named parameters work too
MySQL.update.await('UPDATE users SET job = @job WHERE identifier = @id', { job = 'police', id = identifier })The values travel separately from the SQL text, so they can never change the query’s structure. oxmysql details: the oxmysql guide.
Table and column names
local SORTABLE = { price = 'price', name = 'label', date = 'created_at' }
local function listItems(sortKey, desc)
local column = SORTABLE[sortKey] or 'created_at'
local dir = desc and 'DESC' or 'ASC'
return MySQL.query.await(('SELECT * FROM market ORDER BY %s %s LIMIT 50'):format(column, dir))
endPlaceholders only work for values. When a name must vary, map input to a fixed whitelist and never insert the raw input.
LIKE searches
local term = search:gsub('[%%_\\]', '\\%0')
local rows = MySQL.query.await('SELECT name FROM characters WHERE name LIKE ? LIMIT 20', { term .. '%' })Escape % and _ so a player cannot turn a search into “match everything”, and always add a LIMIT.
Defence in depth
- A database user with rights only on the server’s database.
- Daily backups — see server backups.
- Search old resources for
MySQLcalls with..inside the query string and fix them.
Frequently asked questions
How do I prevent SQL injection in FiveM?
Use oxmysql placeholders (? or @name) for every value and never build queries from player input.
Can I use placeholders for table names?
No. Map input to a fixed whitelist of allowed names instead.
Is string.format safe for SQL?
Not with player-controlled values. Only use it for whitelisted names and constants.
Are old ESX scripts vulnerable?
Some are. Audit queries that concatenate values and rewrite them with placeholders.
Need this built, not just explained?
Ten years of FiveM work, from Lua to NUI
Custom resources, React NUI, ESX / QBCore / Qbox integration, OneSync performance audits and security reviews — plus the websites and SEO around your server brand.
Related guides
- EngineeringUsing oxmysql in FiveM: setup, queries and good habitsPoint oxmysql at your database with set mysql_connection_string "mysql://user:password@localhost:3306/database" in server.cfg, add server_script '@oxmysql/lib/MySQL.lua' to your resource, and call MySQL.query.await, MySQL.single.await, MySQL.scalar.await, MySQL.insert.await, MySQL.update.await or MySQL.prepare.await with ? placeholders. Enable mysql_slow_query_warning to catch slow queries.
- EngineeringWriting server events that cannot be abusedAny client can call any event you registered with RegisterNetEvent, with any arguments. In each handler: copy source into a local, check argument types and ranges, re-derive everything from server state (prices, amounts, rewards), verify the player can do this now (distance, job, item, cooldown), and log refusals. Events only other server scripts should use are registered with AddEventHandler alone, so clients cannot trigger them.
- EngineeringDesigning a database for a FiveM serverGive every table a primary key, reference characters by one stable identifier (citizenid on QBCore, identifier on ESX), index every column you search or join on, use utf8mb4 so names with emoji work, store small flexible data as JSON but put anything you search or count in real columns, and add created_at/updated_at timestamps.
- EngineeringRate limiting server events in FiveMGive every expensive or rewarding server event a per-player limit. A simple cooldown (one call per N seconds) covers most actions; a token bucket allows short bursts with a sustained rate. Key limits by player and event, clear them on playerDropped, log players who hit limits repeatedly, and never rely on client-side delays. FiveM for GTAV Enhanced adds rateLimiter_* convars for built-in network limits.