Защищенные события сервера FiveM: остановите читеров от злоупотребления net событиями
Читеры инициируют ваши события сервера с любыми значениями. Как валидировать на сервере: используйте source, проверьте расстояние, работу и cooldowns и держите цены в таблице сервера.
Скрипт магазина на многих серверах выглядит так, и это бесплатная кнопка денег:
TriggerServerEvent('shop:buy', 'weapon_pistol', 1, -999999)Любой с меню для читеров может отправить эту строку. Сервер запускает обработчик, берет отрицательную цену от игрока и счастливо добавляет деньги. Это руководство показывает, как написать события сервера, которые не доверяют клиенту.
Правило: клиент не доверяется
Все, что приходит в net событие, исходит от машины игрока, которой этот игрок управляет. Обрабатывайте каждый аргумент как ввод пользователя на веб-сайте: это может быть отсутствующим, неправильным типом, отрицательным, огромным или просто выдуманным.
Что сервер может доверять:
source, игрок, который инициировал событие. Движок устанавливает это.- Данные, которые сервер уже держит: работа игрока, инвентарь, деньги, позиция и ваши собственные таблицы конфигурации.
Чему он никогда не должен доверять: количества, цены, имена предметов, ids игроков, координаты, имена работ и флаги «успеха», отправленные клиентом.
Плохой пример
RegisterNetEvent('shop:buy', function(item, count, price)
local xPlayer = ESX.GetPlayerFromId(source)
xPlayer.removeMoney(price * count)
xPlayer.addInventoryItem(item, count)
end)Клиент выбирает предмет, количество и цену. Читер покупает что-нибудь за ничего или отрицательное количество, чтобы напечатать деньги.
Хороший пример
Держите цены на сервере, валидируйте типы, проверьте, где находится игрок, затем действуйте.
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
Только поиск игрока, поле работы и вызов денег изменяются. Валидация остается той же.
-- 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')Проверки работы и разрешения
Если событие должно работать только для работы, проверьте это на сервере из данных фреймворка, а не из аргумента.
-- 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 endДля действий администратора проверьте разрешение ACE вместо флага клиента. Смотрите разрешения ACE:
if not IsPlayerAceAllowed(source, 'command.myadmin') then return endНаграды требуют доказательства на стороне сервера
События как job:finished или mission:reward — это классическая цель. Читер просто запускает их в цикле.
- Держите сумму награды в таблице сервера, никогда в аргументах события.
- Помните на сервере, что игрок начал работу, и платите только если это состояние существует. Очистите его, когда платите.
- Добавьте минимальное время между началом и завершением.
- Проверьте, что игрок находится рядом с точкой доставки, когда они завершают.
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+, команды и живая очередь.Смотреть скрипт →