PiTyUs.Hire me

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.

Updated 9 min readBy PiTyUs · FiveM developer

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

server.lua — vulnerablelua
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

server.lua — safelua
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

server.lua — sorting by a user-chosen columnlua
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))
end

Placeholders only work for values. When a name must vary, map input to a fixed whitelist and never insert the raw input.

LIKE searches

server.lualua
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 MySQL calls 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