Eventos seguros del servidor de FiveM: detén a los tramposos de abusar de eventos de red
Los tramposos disparan tus eventos del servidor con cualquier valor. Cómo validar en el servidor: usa source, verifica distancia, trabajo y cooldowns, y mantén precios en una tabla del servidor.
Un script de tienda en muchos servidores se parece a esto, y es un botón de dinero gratis:
TriggerServerEvent('shop:buy', 'weapon_pistol', 1, -999999)Cualquiera con un menú de trucos puede enviar esa línea. El servidor ejecuta el manejador, toma un precio negativo del jugador y felizmente añade dinero. Esta guía muestra cómo escribir eventos del servidor que no confían en el cliente.
La regla: el cliente no es confiable
Todo lo que llega en un evento de red viene de la máquina de un jugador, que ese jugador controla. Trata cada argumento como entrada de usuario en un sitio web: puede estar faltando, ser del tipo incorrecto, negativo, enorme o simplemente fabricado.
Lo que el servidor puede confiar:
source, el jugador que disparó el evento. El motor lo establece.- Datos que el servidor ya posee: el trabajo del jugador, inventario, dinero, posición, y tus propias tablas de configuración.
Lo que nunca debe confiar: cantidades, precios, nombres de elementos, ids de jugadores, coordenadas, nombres de trabajos y banderas de «éxito» enviadas por el cliente.
Un ejemplo malo
RegisterNetEvent('shop:buy', function(item, count, price)
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.removeMoney(price * count)
xPlayer.addInventoryItem(item, count)
end)El cliente elige el elemento, la cantidad y el precio. Un tramposo compra cualquier cosa por nada, o una cantidad negativa para imprimir dinero.
Un buen ejemplo
Mantén precios en el servidor, valida los tipos, verifica dónde está el jugador, luego actúa.
local Items = {
water = { price = 5, max = 10 },
bread = { price = 8, max = 10 },
}
local shopCoords = vec3(25.7, -1347.3, 29.5)
local lastBuy = {}
RegisterNetEvent('shop:buy', function(item, count)
local src = source
-- 1. types and ranges
if type(item) ~= 'string' or type(count) ~= 'number' then return end
count = math.floor(count)
local def = Items[item]
if not def or count < 1 or count > def.max then return end
-- 2. cooldown
local now = GetGameTimer()
if lastBuy[src] and now - lastBuy[src] < 1000 then return end
lastBuy[src] = now
-- 3. distance
local ped = GetPlayerPed(src)
if #(GetEntityCoords(ped) - shopCoords) > 5.0 then return end
-- 4. price comes from the server table
local total = def.price * count
local xPlayer = ESX.GetPlayerFromId(src)
if not xPlayer or xPlayer.getMoney() < total then return end
xPlayer.removeMoney(total)
xPlayer.addInventoryItem(item, count)
end)
AddEventHandler('playerDropped', function()
lastBuy[source] = nil
end)GetEntityCoords(GetPlayerPed(source)) en el servidor necesita OneSync habilitado. Si aún no lo has hecho, consulta habilitar OneSync.
Lo mismo en QBCore y QBox
Solo la búsqueda del jugador, el campo de trabajo y la llamada de dinero cambian. La validación sigue siendo la misma.
-- QBCore
local QBCore = exports['qb-core']:GetCoreObject()
local Player = QBCore.Functions.GetPlayer(src)
if not Player or Player.PlayerData.money.cash < total then return end
Player.Functions.RemoveMoney('cash', total, 'shop-purchase')-- QBox
local Player = exports.qbx_core:GetPlayer(src)
if not Player or Player.PlayerData.money.cash < total then return end
Player.Functions.RemoveMoney('cash', total, 'shop-purchase')Comprobaciones de trabajo y permiso
Si un evento solo debe funcionar para un trabajo, verifícalo en el servidor desde los datos del framework, no desde un argumento.
-- ESX
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer or xPlayer.job.name ~= 'police' then return end-- QBCore
local Player = QBCore.Functions.GetPlayer(source)
if not Player or Player.PlayerData.job.name ~= 'police' then return endPara acciones de administrador, comprueba un permiso ACE en lugar de una bandera del cliente. Consulta permisos ACE:
if not IsPlayerAceAllowed(source, 'command.myadmin') then return endLas recompensas necesitan prueba del lado del servidor
Eventos como job:finished o mission:reward son el objetivo clásico. Un tramposo simplemente los dispara en un bucle.
- Mantén la cantidad de recompensa en una tabla del servidor, nunca en los argumentos del evento.
- Recuerda en el servidor que el jugador comenzó el trabajo, y solo paga si ese estado existe. Bórralo cuando pagues.
- Añade un tiempo mínimo entre comenzar y terminar.
- Verifica que el jugador esté cerca del punto de entrega cuando termine.
local activeJobs = {}
RegisterNetEvent('job:start', function()
activeJobs[source] = GetGameTimer()
end)
RegisterNetEvent('job:finish', function()
local src = source
local startedAt = activeJobs[src]
if not startedAt or GetGameTimer() - startedAt < 30000 then return end
activeJobs[src] = nil
-- pay a fixed amount defined on the server
end)Otros hábitos que ayudan
- Nunca uses el id de jugador del cliente. Siempre usa
source. Un evento comogiveMoney(playerId, amount)permite que un tramposo pague a cualquiera. - No expongas eventos que no necesites. Un evento registrado solo con
AddEventHandler, sinRegisterNetEvent, no puede ser disparado desde un cliente. - No ejecutes cadenas del cliente como código. Nunca pases un valor del cliente a
load,ExecuteCommando una cadena SQL. Usa consultas parametrizadas, como en la guía de oxmysql. - Registra los casos extraños. Cuando una comprobación falla, imprime el jugador y el nombre del evento. Un patrón de fallos es cómo encuentras tramposos. Banea desde txAdmin, consulta moderar con txAdmin.
Cuidado: ocultar el nombre del evento, ofuscarlo o añadir un «token» del lado del cliente no lo protege. El cliente puede leer todo eso. Solo las comprobaciones en el servidor cuentan.
Lista de verificación
| Síntoma | Solución |
|---|---|
| El cliente envía el precio o la cantidad | Mantén una tabla de precios en el servidor y calcula el total allí |
| El cliente envía un id de jugador | Usa source en su lugar |
| El nombre del elemento viene del cliente | Acepta solo nombres encontrados en una tabla del servidor |
| Cantidades negativas o enormes | Comprueba type, math.floor, y un mínimo y máximo |
| Evento disparado desde el otro lado del mapa | Compara GetEntityCoords(GetPlayerPed(source)) con la posición objetivo |
| Evento enviado como spam en un bucle | Cooldown por jugador, borrado en playerDropped |
| Acción solo para trabajos | Lee el trabajo desde el framework en el servidor |
| Evento de recompensa abusado | Almacena el estado del trabajo en el servidor y paga una cantidad fija una vez |
Respuestas rápidas
¿Puede un jugador realmente disparar mis eventos del servidor?
Sí. Cualquier evento registrado con RegisterNetEvent puede ser disparado por un cliente modificado con cualquier argumento. El servidor no puede distinguir una llamada de script real de una falsificada, así que tiene que validar todo.
¿Es seguro usar un id de jugador enviado por el cliente?
No. En el servidor, source es establecido por el motor y no puede ser falsificado. Un id de jugador, nombre o identificador enviado como argumento puede serlo, así que nunca lo uses para decidir quién recibe pago o castigo.
¿Necesito un anticheat si valido mis eventos?
La validación es la base y detiene la mayoría de abusos de tus propios scripts. Un anticheat añade detección para cosas que los eventos no pueden ver, como menús inyectados. Consulta [lo básico de anticheat de FiveM](/blog/fivem-anticheat-basics).
Scripts que evitan este problema
Shop CreatorCrea una tienda en menos de un minuto — dueños, empleados, caja fuerte y atracos.Ver script →
Item Creator V2Crea items usables con animaciones, props, efectos y más — sin escribir código.Ver script →
Advanced BoostingBoosting de vehículos desde una tablet: contratos de clase D a S+, crews y cola en vivo.Ver script →