QBCore boss menu : RegisterNetEvent nu + lecture-puis-écriture sur la caisse = deux exploits classiques
Contexte
Audit d'un boss menu QBCore (job police) avec caisse de société stockée en table SQL custom (society_funds(job_name, amount)).
Piège 1 — l'event serveur nu
Un pattern très répandu :
RegisterNetEvent('res:server:SetGrade', function(csn, level)
local Target = QBCore.Functions.GetPlayerByCitizenId(csn)
Target.Functions.SetJob('police', tonumber(level))
end)
Le menu vérifie job.grade.level >= Config.BossGrade côté client, donc l'auteur considère l'event comme protégé. Il ne l'est pas : TriggerServerEvent est déclenchable depuis n'importe quel exécuteur Lua. Le contrôle client n'est qu'une décoration d'UI.
Correctif : re-résoudre le boss côté serveur à chaque handler —
local function GetBoss(src)
local Player = QBCore.Functions.GetPlayer(src)
if not Player then return end
local job = Player.PlayerData.job
if job.name ~= Config.Job or not job.isboss then return end
return Player
end
…et y ajouter la protection hiérarchique (impossible d'agir sur un grade.level >= le sien, impossible de promouvoir à son propre niveau), sinon un grade boss « lieutenant » peut dégrader le chef.
Piège 2 — read-then-write sur les fonds
if GetSocietyFund(job) >= amount then -- SELECT
MySQL.update('UPDATE funds SET amount = amount - ? WHERE job_name = ?', {amount, job})
Player.Functions.AddMoney('cash', amount)
end
Les deux requêtes ne sont pas atomiques : en spammant l'event, plusieurs SELECT passent avant le premier UPDATE ⇒ solde négatif (retrait illimité).
Correctif vérifié — mettre la condition DANS l'UPDATE et se fier aux lignes affectées (oxmysql MySQL.update.await retourne affectedRows) :
local rows = MySQL.update.await(
'UPDATE funds SET amount = amount - ? WHERE job_name = ? AND amount >= ?',
{ amount, job, amount })
if (rows or 0) == 0 then return end -- fonds insuffisants, rien n'a bougé
if not Player.Functions.AddMoney('cash', amount) then
MySQL.update.await('UPDATE funds SET amount = amount + ? WHERE job_name = ?', { amount, job }) -- rollback
end
Même logique pour le dépôt : RemoveMoney d'abord, rollback AddMoney si l'UPDATE SQL échoue.
Bonus : requête des employés
WHERE job LIKE '%police%' sur la colonne JSON players.job matche aussi police_academy / unemployed_police et interdit tout index. Préférer
WHERE JSON_UNQUOTE(JSON_EXTRACT(p.job, '$.name')) = ? (MySQL 5.7+ / MariaDB 10.2+).
Tags: qbcore, security, boss-menu, oxmysql, exploits, society-funds
Category: qbcore
Created: 2026-07-13T00:46:10.366Z