Защищенные события сервера FiveM: остановите читеров от злоупотребления net событиями

Читеры инициируют ваши события сервера с любыми значениями. Как валидировать на сервере: используйте source, проверьте расстояние, работу и cooldowns и держите цены в таблице сервера.

Скрипт магазина на многих серверах выглядит так, и это бесплатная кнопка денег:

text
TriggerServerEvent('shop:buy', 'weapon_pistol', 1, -999999)

Любой с меню для читеров может отправить эту строку. Сервер запускает обработчик, берет отрицательную цену от игрока и счастливо добавляет деньги. Это руководство показывает, как написать события сервера, которые не доверяют клиенту.

Правило: клиент не доверяется

Все, что приходит в net событие, исходит от машины игрока, которой этот игрок управляет. Обрабатывайте каждый аргумент как ввод пользователя на веб-сайте: это может быть отсутствующим, неправильным типом, отрицательным, огромным или просто выдуманным.

Что сервер может доверять:

  • source, игрок, который инициировал событие. Движок устанавливает это.
  • Данные, которые сервер уже держит: работа игрока, инвентарь, деньги, позиция и ваши собственные таблицы конфигурации.

Чему он никогда не должен доверять: количества, цены, имена предметов, ids игроков, координаты, имена работ и флаги «успеха», отправленные клиентом.

Плохой пример

lua
RegisterNetEvent('shop:buy', function(item, count, price)
    local xPlayer = ESX.GetPlayerFromId(source)
    xPlayer.removeMoney(price * count)
    xPlayer.addInventoryItem(item, count)
end)

Клиент выбирает предмет, количество и цену. Читер покупает что-нибудь за ничего или отрицательное количество, чтобы напечатать деньги.

Хороший пример

Держите цены на сервере, валидируйте типы, проверьте, где находится игрок, затем действуйте.

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)) на сервере требует включенного OneSync. Если вы этого еще не сделали, смотрите включение OneSync.

То же самое на QBCore и QBox

Только поиск игрока, поле работы и вызов денег изменяются. Валидация остается той же.

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

Проверки работы и разрешения

Если событие должно работать только для работы, проверьте это на сервере из данных фреймворка, а не из аргумента.

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

Для действий администратора проверьте разрешение ACE вместо флага клиента. Смотрите разрешения ACE:

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

Награды требуют доказательства на стороне сервера

События как job:finished или mission:reward — это классическая цель. Читер просто запускает их в цикле.

  • Держите сумму награды в таблице сервера, никогда в аргументах события.
  • Помните на сервере, что игрок начал работу, и платите только если это состояние существует. Очистите его, когда платите.
  • Добавьте минимальное время между началом и завершением.
  • Проверьте, что игрок находится рядом с точкой доставки, когда они завершают.
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)

Другие привычки, которые помогают

  • Никогда не используйте id игрока клиента. Всегда используйте source. Событие как giveMoney(playerId, amount) позволяет читеру платить кому-нибудь.
  • Не выставляйте события, которые вам не нужны. Событие, зарегистрированное только с AddEventHandler, без RegisterNetEvent, не может быть инициировано от клиента.
  • Не запускайте строки клиента как код. Никогда не передавайте значение клиента load, ExecuteCommand или строку SQL. Используйте параметризованные запросы, как в руководстве oxmysql.
  • Логируйте странные случаи. Когда проверка не срабатывает, выведите игрока и имя события. Паттерн отказов — это как вы находите читеров. Забаньте из txAdmin, смотрите модерирование с txAdmin.

Внимание: скрытие имени события, запутывание его или добавление клиентского «токена» не защищает его. Клиент может все это прочитать. Только проверки на сервере имеют значение.

Контрольный список

Симптом Исправление
Клиент отправляет цену или сумму Держите таблицу цен на сервере и вычислите общую там
Клиент отправляет id игрока Используйте source вместо этого
Имя предмета приходит от клиента Принимайте только имена, найденные в таблице сервера
Отрицательные или огромные суммы Проверьте type, math.floor и минимум и максимум
Событие запущено с другой стороны карты Сравните GetEntityCoords(GetPlayerPed(source)) с целевой позицией
Событие спамировано в цикле Cooldown для игрока, очищенный в playerDropped
Действие только для работы Прочитайте работу из фреймворка на сервере
Событие награды злоупотреблено Сохраняйте состояние работы на сервере и платите фиксированную сумму один раз

Короткие ответы

Может ли игрок действительно инициировать мои события сервера?

Да. Любое событие, зарегистрированное с RegisterNetEvent, может быть инициировано измененным клиентом с любыми аргументами. Сервер не может отличить реальный вызов скрипта от поддельного, поэтому он должен проверить все.

Безопасно ли использовать id игрока, отправленный клиентом?

Нет. На сервере source установлен движком и не может быть подделан. Id игрока, имя или идентификатор, отправленные как аргумент, могут быть, поэтому никогда не используйте его для решения, кто получает оплату или наказание.

Нужен ли мне anticheat, если я валидирую свои события?

Валидация — это база и останавливает большинство злоупотреблений вашими собственными скриптами. Anticheat добавляет обнаружение для вещей, которые события не могут видеть, таких как внедренные меню. Смотрите [основы FiveM anticheat](/blog/fivem-anticheat-basics).

Скрипты без этой проблемы

Shop CreatorМагазин меньше чем за минуту — владельцы, сотрудники, сейфы и ограбления в комплекте.Смотреть скрипт →Item Creator V2Создавайте используемые предметы с анимациями, пропами, эффектами и не только — без кода.Смотреть скрипт →Advanced BoostingУгон машин на заказ через планшет: контракты от класса D до S+, команды и живая очередь.Смотреть скрипт →

Читайте также