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:

text
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

lua
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.

lua
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.

lua
-- 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')
lua
-- 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.

lua
-- ESX
local xPlayer = ESX.GetPlayerFromId(source)
if not xPlayer or xPlayer.job.name ~= 'police' then return end
lua
-- QBCore
local Player = QBCore.Functions.GetPlayer(source)
if not Player or Player.PlayerData.job.name ~= 'police' then return end

Para acciones de administrador, comprueba un permiso ACE en lugar de una bandera del cliente. Consulta permisos ACE:

lua
if not IsPlayerAceAllowed(source, 'command.myadmin') then return end

Las 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.
lua
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 como giveMoney(playerId, amount) permite que un tramposo pague a cualquiera.
  • No expongas eventos que no necesites. Un evento registrado solo con AddEventHandler, sin RegisterNetEvent, no puede ser disparado desde un cliente.
  • No ejecutes cadenas del cliente como código. Nunca pases un valor del cliente a load, ExecuteCommand o 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 →

Sigue leyendo