RU/Script security
Внимание к клиентской памяти
Начнем с самых основ:
- Вы должны знать, что все, что вы храните на стороне клиента, подвержено риску, в том числе и lua-файлы. Злоумышленники могут получить доступ к любым конфиденциальным (и/или) важным данным, которые хранятся или передаются на стороне клиента (ПК игрока).
- Для обеспечения безопасности конфиденциальных данных (и/или) логики Lua используйте серверную часть.
- Обратите внимание, что скрипты, помеченные как shared, также действуют как client code, что означает, что к ним применимо все вышесказанное. Например, определение:
<script src="script.lua" type="shared"/> <!-- этот скрипт будет выполняться отдельно как на клиенте, так и на сервере -->
Это то же самое, что делать:
<script src="script.lua" type="client"/> <!-- define it separately on client --> <script src="script.lua" type="server"/> <!-- do the same, but on server -->
Дополнительный уровень защиты
Чтобы немного усложнить задачу для тех, у кого плохие намерения по отношению к вашему серверу, вы можете использовать атрибут cache (и/или / Lua compile (также известный как Luac) с дополнительной настройкой запутывания на уровне 3 - API), доступный в meta.xml , а также о настройке встроенного AC в MTA путем переключения SD (Специальные средства обнаружения), смотрите: Руководство по борьбе с читерством.
<script src="shared.lua" type="shared" cache="false"/> <!-- cache="false" означает, что этот Lua-файл не будет сохранен на ПК игрока --> <script src="client.lua" type="client" cache="false"/> <!-- cache="false" означает, что этот Lua-файл не будет сохранен на ПК игрока -->
- Для некоторых немного сложнее, но не невозможно, получить клиентский код, но при этом он отлично справляется с тем, что удерживает большинство пользователей от проверки ваших lua-файлов - тех, кто ищет возможные логические ошибки (баги) или отсутствующие / некорректные проверки на основе безопасности.
- Может использоваться как для клиентских (client), так и для общих (shared) скриптов (не влияет на Lua на стороне сервера).
- Не удаляет файлы Lua, которые были загружены ранее.
Обнаружение бэкдоров и читерских программ и борьба с ними
Для обеспечения минимального ущерба (или его отсутствия) от Lua-скриптов:
- Поддерживайте свой сервер в актуальном состоянии, вы можете загружать последние сборки сервера с сайта MTA:SA nightly. Информацию о конкретных сборках можно найти здесь.
- Поддерживайте свои ресурсы в актуальном состоянии, вы можете \ загружать последние (по умолчанию) ресурсы из репозитория GitHub. Они часто содержат последние исправления для системы безопасности, что может привести к тому, что ваш сервер будет защищен от атак или нет.
- Убедитесь в правильной настройке ACL (Список контроля доступа), который будет блокировать использование ресурсами определенных потенциально опасных функций.
- Нулевое доверие с предоставлением прав администратора для ресурсов (включая карты), поступающих из неизвестных источников.
- Перед запуском любого ресурса, которому вы не доверяете, проанализируйте его:
- meta.xml на предмет возможных скрытых скриптов, скрывающихся под другими расширениями файлов.
- Это исходный код, на предмет вредоносной логики.
- Не запускайте и не продолжайте использовать скомпилированные ресурсы (скрипты), в легитимности которых вы не уверены.
Чтобы обеспечить минимальный ущерб при подключении мошенника к вашему серверу:
- При создании скриптов помните, что никогда не следует доверять данным, поступающим от клиента.
- При проверке скриптов на предмет возможных дыр в системе безопасности. Обратите внимание на любые данные, поступающие от клиента, которым вы доверяете.
- Могут быть отправлены любые данные, следовательно, серверные скрипты, которые взаимодействуют с клиентом, получая данные, отправленные игроками, должны проверить их перед дальнейшим использованием в последующих частях кода. В основном, это будет сделано либо с помощью setElementData, либо с помощью triggerServerEvent.
- Вам не следует полагаться только на игрока серийный номер , когда речь заходит о выполнении важных операций (авторегистрация/действия администратора). Не гарантируется, что серийные номера будут уникальными или не поддающимися подделке. Вот почему вам следует "оставить это позади" системы учетных записей, как важный фактор аутентификации (например, логин и пароль).
- Логика на стороне сервера не может быть обойдена или изменена (если только сервер не взломан или если в коде есть ошибка, но это совсем другой сценарий) - "используйте это в своих интересах". В большинстве случаев вы сможете выполнить проверку безопасности без участия клиента.
- Используя концепцию ""'“, все параметры, включая источник, могут быть подделаны, и им не следует доверять. Глобальной переменной client можно доверять.”""" - дает вам надежную гарантию того, что игрок, на которого вы ссылаетесь (через ""client"") ""на самом деле""тот, кто действительно вызвал событие"". Такой подход защитит вас от ситуаций, когда мошенник может вызывать и обрабатывать, например, события администратора (например, кикнуть/забанить игрока), передавая фактический администратор ("'2-й"' аргумент в "'triggerServerEvent"'), или запускать события для других целей. игроки (как будто это они их вызвали, но на самом деле это было принудительно сделано читером) - как следствие использования неправильной переменной. Чтобы убедиться, что вы полностью поняли это, взгляните на примеры ниже:
--[[
НИКОГДА НЕ ДЕЛАЙТЕ ЭТОГО - ЭТО СОВЕРШЕННО НЕПРАВИЛЬНО И НЕБЕЗОПАСНО
ПРОБЛЕМА: ИСПОЛЬЗУЯ "source" В hasObjectPermissionTo, ВЫ ОСТАВЛЯЕТЕ ДВЕРЬ ОТКРЫТОЙ ДЛЯ МОШЕННИКОВ
]]
function onServerWrongAdminEvent(playerToBan)
if (not client) then -- "client" указывает на игрока, который инициировал событие, и должен использоваться в качестве меры безопасности (чтобы предотвратить подделку игрока).
return false -- если эта переменная в данный момент не существует (по неизвестной причине, или это сервер инициировал это событие), остановите выполнение кода
end
local defaultPermission = false -- не разрешайте действие по умолчанию, смотрите (defaultPermission): https://wiki.multitheftauto.com/wiki/HasObjectPermissionTo
local canAdminBanPlayer = hasObjectPermissionTo(source, "function.banPlayer", defaultPermission) -- уязвимость кроется именно здесь...
if (not canAdminBanPlayer) then -- если у игрока нет прав доступа
return false -- не делайте этого
end
local validElement = isElement(playerToBan) -- проверяет, является ли аргумент, переданный от клиента, элементом
if (not validElement) then -- it is not
return false -- остановить обработку кода
end
local elementType = getElementType(playerToBan) -- это элемент, поэтому укажите его тип
local playerType = (elementType == "player") -- убедитесь, что это игрок
if (not playerType) then -- it's not a player
return false - остановись здесь
end
-- banPlayer(...) -- do what needs to be done
end
addEvent("onServerWrongAdminEvent", true)
addEventHandler("onServerWrongAdminEvent", root, onServerWrongAdminEvent)
--[[
onServerCorrectAdminEvent ПОЛНОСТЬЮ ЗАЩИЩЕН, КАК И ДОЛЖНО БЫТЬ
ЗДЕСЬ НЕТ ПРОБЛЕМ: МЫ ИСПОЛЬЗОВАЛИ "клиент" В hasObjectPermissionTo, ЧТО ПОЗВОЛЯЕТ ЗАЩИТИТЬ ЕГО ОТ ПОДДЕЛКИ ИГРОКОМ, ВЫЗВАВШИМ СОБЫТИЕ
]]
function onServerCorrectAdminEvent(playerToBan)
if (not client) then -- 'client' points to the player who triggered the event, and should be used as security measure (in order to prevent player faking)
return false -- if this variable doesn't exists at the moment (for unknown reason, or it was the server who triggered this event), stop code execution
end
local defaultPermission = false -- do not allow action by default, see (defaultPermission): https://wiki.multitheftauto.com/wiki/HasObjectPermissionTo
local canAdminBanPlayer = hasObjectPermissionTo(client, "function.banPlayer", defaultPermission) -- is player allowed to do that?
if (not canAdminBanPlayer) then -- if player doesn't have permissions
return false -- don't do it
end
local validElement = isElement(playerToBan) -- check whether argument passed from client is an element
if (not validElement) then -- it is not
return false -- stop code processing
end
local elementType = getElementType(playerToBan) -- it's an element, so get it's type
local playerType = (elementType == "player") -- make sure that it's a player
if (not playerType) then -- it's not a player
return false -- stop here
end
-- banPlayer(...) -- do what needs to be done
end
addEvent("onServerCorrectAdminEvent", true)
addEventHandler("onServerCorrectAdminEvent", root, onServerCorrectAdminEvent)
Защита данных setElementData
- Вам следует воздержаться от использования element data везде, его следует использовать только тогда, когда это действительно необходимо. Рекомендуется заменить его на triggerClientEvent.
- Если вы все еще настаиваете на его использовании, рекомендуется установить для 4-го аргумента в setElementData значение false (отключив синхронизацию для этих определенных данных) и использовать режим подписчика - addElementDataSubscriber, как по соображениям безопасности, так и по соображениям производительности, описанным в разделе "события".
- Все параметры, включая source, могут быть подделаны, и им не следует доверять.
- Глобальной переменной client можно доверять.
Пример защиты данных базового элемента от обмана.
local function reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue) -- вспомогательная функция для логирования и отката изменений
local logClient = inspect(clientElement) -- подробный вид игрока, который инициировал синхронизацию данных элемента
local logSerial = getPlayerSerial(clientElement) or "N/A" -- серийник клиента, или "N/A", если по какой-то причине это невозможно
local logSource = tostring(sourceElement) -- элемент, который получил данные
local logOldValue = tostring(oldValue) -- старое значение
local logNewValue = tostring(newValue) -- новое значение
local logText = -- заполняем наш отчет данными
"=======================================\n"..
"Detected element data abnormality:\n"..
"Client: "..logClient.."\n"..
"Client serial: "..logSerial.."\n"..
"Source: "..logSource.."\n"..
"Data key: "..dataKey.."\n"..
"Old data value: "..logOldValue.."\n"..
"New data value: "..logNewValue.."\n"..
"======================================="
local logVisibleTo = root -- указываем, кто увидит этот лог в консоли, в данном случае каждый игрок, подключенный к серверу
local hadData = (oldValue ~= nil) -- проверяем, был ли у элемента такой данные раньше
if (hadData) then -- если у элемента ранее были такие данные
setElementData(sourceElement, dataKey, oldValue, true) -- откатываем изменения, это вызовет событие onElementDataChange, но не пройдет (остановится) на первом условии - потому что сервер (не клиент) принудительно изменил данные
else
removeElementData(sourceElement, dataKey) -- полностью удаляем его
end
outputConsole(logText, logVisibleTo) -- выводим это в консоль
return true -- все успешно
end
function onElementDataChangeBasicAC(dataKey, oldValue, newValue) -- сердце нашего античита, которое выполняет все магические меры безопасности
if (not client) then -- проверяем, идут ли данные от клиента
return false -- если нет, не идем дальше
end
local checkSpecialThing = (dataKey == "special_thing") -- сравниваем, совпадает ли dataKey с "special_thing"
local checkFlagWaving = (dataKey == "flag_waving") -- сравниваем, совпадает ли dataKey с "flag_waving"
if (checkSpecialThing) then -- если совпадает, проводим проверки безопасности
local invalidElement = (client ~= source) -- проверяем, отличается ли исходный элемент от игрока, изменившего данные
if (invalidElement) then -- если так и есть
reportAndRevertDataChange(client, source, dataKey, oldValue, newValue) -- откатываем изменение данных, потому что "special_thing" может быть установлен только для самого игрока
end
end
if (checkFlagWaving) then -- если совпадает, проводим проверки безопасности
local playerVehicle = getPedOccupiedVehicle(client) -- получаем текущий транспорт игрока
local invalidVehicle = (playerVehicle ~= source) -- проверяем, отличается ли исходный элемент от транспорта игрока
if (invalidVehicle) then -- если так и есть
reportAndRevertDataChange(client, source, dataKey, oldValue, newValue) -- откатываем изменение данных, потому что "flag_waving" может быть установлен только для собственного транспорта игрока
end
end
end
addEventHandler("onElementDataChange", root, onElementDataChangeBasicAC)
Пример расширенной защиты данных element от обмана.
--[[
Для максимальной безопасности установите punishPlayerOnDetect, punishmentBan, allowOnlyProtectedKeys в true (как настроено по умолчанию)
Если включен allowOnlyProtectedKeys, не забудьте добавить каждый ключ элемент данных на стороне клиента в таблицу protectedKeys - иначе возникнут ложные срабатывания
Структура таблицы античита (handleDataChange) и её параметры:
["keyName"] = { -- имя ключа, который будет защищен
onlyForPlayerHimself = true, -- включение этого (true) гарантирует, что этот ключ данных элемента может быть установлен только игроком, который его синхронизировал (игнорирует onlyForOwnPlayerVeh и allowForElements), используйте false/nil для отключения
onlyForOwnPlayerVeh = false, -- включение этого (true) гарантирует, что этот ключ данных элемента может быть установлен только на текущем транспорте игрока, который его синхронизировал (игнорирует allowForElements), используйте false/nil для отключения
allowForElements = { -- ограничить этот ключ для определенных типов элементов, установите false/nil или оставьте пустым, чтобы не проверять это (полный список типов элементов: https://wiki.multitheftauto.com/wiki/GetElementsByType)
["player"] = true,
["ped"] = true,
["vehicle"] = true,
["object"] = true,
},
allowedDataTypes = { -- ограничить этот ключ для определенных типов значений, установите false/nil или оставьте пустым, чтобы не проверять это
["string"] = true,
["number"] = true,
["table"] = true,
["boolean"] = true,
["nil"] = true,
},
allowedStringLength = {1, 32}, -- если значение является строкой, то её длина должна быть в пределах (мин-макс), установите false/nil, чтобы не проверять длину строки - учтите, что allowedDataTypes должен содержать ["string"] = true
allowedTableLength = {1, 64}, -- если значение является таблицей, то её длина должна быть в пределах (мин-макс), установите false/nil, чтобы не проверять длину таблицы - учтите, что allowedDataTypes должен содержать ["table"] = true
allowedNumberRange = {1, 128}, -- если значение является числом, то оно должно быть в пределах (мин-макс), установите false/nil, чтобы не проверять диапазон чисел - учтите, что allowedDataTypes должен содержать ["number"] = true
}
]]
local punishPlayerOnDetect = true -- должен ли игрок быть наказан при обнаружении (убедитесь, что ресурс, запускающий этот код, имеет права администратора)
local punishmentBan = true -- актуально только если punishPlayerOnDetect установлен в true; используйте true для бана или false для кика
local punishmentReason = "Altering element data" -- актуально только если punishPlayerOnDetect установлен в true; причина, которая будет показана наказанному игроку
local punishedBy = "Console" -- актуально только если punishPlayerOnDetect установлен в true; кто ответственен за наказание, также показывается наказанному игроку
local banByIP = false -- актуально только если punishPlayerOnDetect и punishmentBan установлены в true; бан по IP в настоящее время не рекомендуется (...)
local banByUsername = false -- имя пользователя сообщества - устаревшая вещь, поэтому установлено в false и должно оставаться таким
local banBySerial = true -- актуально только если punishPlayerOnDetect и punishmentBan установлены в true; (...) если есть серийный номер игрока, который можно использовать вместо этого
local banTime = 0 -- актуально только если punishPlayerOnDetect и punishmentBan установлены в true; время в секундах, 0 навсегда
local allowOnlyProtectedKeys = true -- запрещать (удалять с помощью removeElementData) любые данные элементов, кроме тех, что указаны в таблице protectedKeys; на случай, если кто-то захочет завалить сервер мусорными ключами, которые будут храниться в памяти до перезапуска сервера или ручного удаления - setElementData(source, key, nil) не удалит их; необходимо использовать removeElementData
local debugLevel = 4 -- этот уровень отладки позволяет скрыть префикс INFO: и использовать пользовательские цвета
local debugR = 255 -- сообщение отладки - красный цвет
local debugG = 127 -- сообщение отладки - зеленый цвет
local debugB = 0 -- сообщение отладки - синий цвет
local protectedKeys = {
["vehicleNumber"] = { -- мы хотим, чтобы vehicleNumber устанавливался только на транспорте, с четкими числами в диапазоне 1-100
allowForElements = {
["vehicle"] = true,
},
allowedDataTypes = {
["number"] = true,
},
allowedNumberRange = {1, 100},
},
["personalVehData"] = { -- мы хотим иметь возможность устанавливать personalVehData только на текущем транспорте, также это должна быть строка длиной от 1 до 24
onlyForOwnPlayerVeh = true,
allowedDataTypes = {
["string"] = true,
},
allowedStringLength = {1, 24},
},
-- выполнять проверки безопасности для ключей, хранящихся в этой таблице, удобным способом
}
local function reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- вспомогательная функция для логирования и отката изменений
local logClient = inspect(clientElement) -- подробный вид игрока, который инициировал синхронизацию данных элемента
local logSerial = getPlayerSerial(clientElement) or "N/A" -- серийник клиента, или "N/A", если по какой-то причине это невозможно
local logSource = inspect(sourceElement) -- подробный вид элемента, который получил данные
local logOldValue = inspect(oldValue) -- подробный вид старого значения
local logNewValue = inspect(newValue) -- подробный вид нового значения
local logText = -- заполняем наш отчет данными
"*\n"..
"Detected element data abnormality:\n"..
"Client: "..logClient.."\n"..
"Client serial: "..logSerial.."\n"..
"Source: "..logSource.."\n"..
"Data key: "..dataKey.."\n"..
"Old data value: "..logOldValue.."\n"..
"New data value: "..logNewValue.."\n"..
"Fail reason: "..failReason.."\n"..
"*"
outputDebugString(logText, debugLevel, debugR, debugG, debugB) -- выводим это в отладку
if (forceRemove) then -- мы вообще не хотим, чтобы этот ключ данных элемента существовал
removeElementData(sourceElement, dataKey) -- удаляем его
return true -- возвращаем успех и останавливаемся здесь, дальнейшие проверки не нужны
end
setElementData(sourceElement, dataKey, oldValue, true) -- откатываем изменения, это вызовет событие onElementDataChange, но не пройдет (остановится) на первом условии - потому что сервер (не клиент) принудительно изменил данные
return true -- возвращаем успех
end
local function handleDataChange(clientElement, sourceElement, dataKey, oldValue, newValue) -- сердце нашего античита, которое выполняет все магические меры безопасности, возвращает true, если не было изменений от клиента (на основе данных из protectedKeys), иначе false
local protectedKey = protectedKeys[dataKey] -- проверяем, хранится ли измененный ключ в таблице protectedKeys
if (not protectedKey) then -- если его нет
if (allowOnlyProtectedKeys) then -- если мы не хотим мусорные ключи
local failReason = "Key isn't present in protectedKeys" -- причина, показанная в отчете
local forceRemove = true -- должен ли ключ быть полностью удален
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
return true -- этот ключ не защищен, пропускаем его
end
local onlyForPlayerHimself = protectedKey.onlyForPlayerHimself -- если у ключа есть блокировка "только для себя"
local onlyForOwnPlayerVeh = protectedKey.onlyForOwnPlayerVeh -- если у ключа есть блокировка "свой транспорт"
local allowForElements = protectedKey.allowForElements -- если у ключа есть проверка типа элемента
local allowedDataTypes = protectedKey.allowedDataTypes -- если у ключа есть проверка разрешенного типа данных
if (onlyForPlayerHimself) then -- если блокировка "только для себя" активна
local matchingElement = (clientElement == sourceElement) -- проверяем, равен ли игрок, установивший данные, элементу, который получил данные
if (not matchingElement) then -- если не совпадает
local failReason = "Can only set on player himself" -- причина, показанная в отчете
local forceRemove = false -- должен ли ключ быть полностью удален
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
end
if (onlyForOwnPlayerVeh) then -- если блокировка "свой транспорт" активна
local playerVehicle = getPedOccupiedVehicle(clientElement) -- получить текущий транспорт игрока, который установил данные
local matchingVehicle = (playerVehicle == sourceElement) -- проверяем, совпадает ли он с тем, который получил данные
if (not matchingVehicle) then -- если не совпадает
local failReason = "Can only set on player's own vehicle" -- причина, показанная в отчете
local forceRemove = false -- должен ли ключ быть полностью удален
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
end
if (allowForElements) then -- проверяем, является ли оно одним из них
local elementType = getElementType(sourceElement) -- получить тип элемента, данные которого изменились
local matchingElementType = allowForElements[elementType] -- проверяем, разрешено ли это
if (not matchingElementType) then -- это не совпадает
local failReason = "Invalid element type" -- причина, показанная в отчете
local forceRemove = false -- должен ли ключ быть полностью удален
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
end
if (allowedDataTypes) then -- если есть разрешенные типы данных
local valueType = type(newValue) -- получить тип данных значения
local matchingType = allowedDataTypes[valueType] -- проверяем, является ли оно одним из разрешенных
if (not matchingType) then -- если нет, тогда
local failReason = "Invalid data type" -- причина, показанная в отчете
local forceRemove = false -- должен ли ключ быть полностью удален
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
local allowedStringLength = protectedKey.allowedStringLength -- если у ключа есть указанная проверка длины строки
local dataString = (valueType == "string") -- убеждаемся, что это строка
if (allowedStringLength and dataString) then -- если мы должны проверить длину строки
local minLength = allowedStringLength[1] -- получить минимальную длину
local maxLength = allowedStringLength[2] -- получить максимальную длину
local stringLength = utf8.len(newValue) -- получить длину строки данных
local matchingLength = (stringLength >= minLength) and (stringLength <= maxLength) -- сравниваем, подходит ли значение в промежуток
if (not matchingLength) then -- если не подходит
local failReason = "Invalid string length (must be between "..minLength.."-"..maxLength..")" -- причина, показанная в отчете
local forceRemove = false -- должен ли ключ быть полностью удален
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
end
local allowedTableLength = protectedKey.allowedTableLength -- если у ключа есть проверка длины таблицы
local dataTable = (valueType == "table") -- убеждаемся, что это таблица
if (allowedTableLength and dataTable) then -- если мы должны проверить длину таблицы
local minLength = allowedTableLength[1] -- получить минимальную длину
local maxLength = allowedTableLength[2] -- получить максимальную длину
local minLengthAchieved = false -- переменная, которая проверяет 'была ли достигнута минимальная длина'
local maxLengthExceeded = false -- переменная, которая проверяет 'превысила ли длина разрешенный максимум'
local tableLength = 0 -- сохранить начальную длину таблицы
for _, _ in pairs(newValue) do -- проходим по всей таблице
tableLength = (tableLength + 1) -- добавляем + 1 для каждой записи таблицы
minLengthAchieved = (tableLength >= minLength) -- больше ли длина или по крайней мере такой минимум, который мы требуем
maxLengthExceeded = (tableLength > maxLength) -- превысила ли таблица максимальную длину?
if (maxLengthExceeded) then -- она больше, чем должна быть
break -- прерываем цикл (из-за того, что условие выше выполнено, нет смысла считать дальше и тратить процессор на таблицу, которая потенциально может иметь огромное количество записей)
end
end
local matchingLength = (minLengthAchieved and not maxLengthExceeded) -- проверяем, достигнута ли минимальная длина, и убеждаемся, что она не превышает максимальную
if (not matchingLength) then -- эта таблица не соответствует требованиям
local failReason = "Invalid table length (must be between "..minLength.."-"..maxLength..")" -- причина, показанная в отчете
local forceRemove = false -- должен ли ключ быть полностью удален
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
end
local allowedNumberRange = protectedKey.allowedNumberRange -- если у ключа есть проверка разрешенного диапазона чисел
local dataNumber = (valueType == "number") -- убеждаемся, что это число
if (allowedNumberRange and dataNumber) then -- если мы должны проверить диапазон чисел
local minRange = allowedNumberRange[1] -- получить минимальный диапазон чисел
local maxRange = allowedNumberRange[2] -- получить максимальный диапазон чисел
local matchingRange = (newValue >= minRange) and (newValue <= maxRange) -- сравниваем, подходит ли значение в промежуток
if (not matchingRange) then -- если не подходит
local failReason = "Invalid number range (must be between "..minRange.."-"..maxRange..")" -- причина, показанная в отчете
local forceRemove = false -- должен ли ключ быть полностью удален
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
end
end
return true -- проверки безопасности пройдены, с этим ключом данных всё в порядке
end
function onElementDataChangeAdvancedAC(dataKey, oldValue, newValue) -- это событие использует handleDataChange, код был разделен для лучшей читаемости
if (not client) then -- проверяем, идут ли данные от клиента
return false -- если нет, не продолжаем
end
local approvedChange = handleDataChange(client, source, dataKey, oldValue, newValue) -- запускаем наши проверки безопасности
if (approvedChange) then -- всё круто и хорошо
return false -- нам не нужны дальнейшие действия
end
if (not punishPlayerOnDetect) then -- мы не хотим наказывать игрока по какой-то причине
return false -- так что останавливаемся здесь
end
if (punishmentBan) then -- если это бан
banPlayer(client, banByIP, banByUsername, banBySerial, punishedBy, punishmentReason, banTime) -- удаляем его присутствие с сервера
else -- иначе
kickPlayer(client, punishedBy, punishmentReason) -- просто кикаем игрока с сервера
end
end
addEventHandler("onElementDataChange", root, onElementDataChangeAdvancedAC)
Защита triggerServerEvent
- Все параметры, включая "source", могут быть подделаны, и им не следует доверять.
- Глобальной переменной "client" можно доверять.
- "События" в стиле "Admin" должны подтверждать "права доступа" игрока (клиента) либо с помощью isObjectInACLGroup, либо hasObjectPermissionTo.
- Не используйте для своего пользовательского события то же имя, что и для событий собственного сервера MTA (которые по умолчанию не запускаются удаленно), например: "onPlayerLogin"; это откроет дверь для манипуляций мошенников.
- Будьте в курсе, каким игрокам отправляется событие через triggerClientEvent. Как по соображениям безопасности, так и по соображениям производительности, события, подобные admin, должны приниматься только администраторами (чтобы предотвратить доступ к конфиденциальным данным), в то же время вы не должны отправлять каждое событие всем пользователям сервера (например, событие успешного входа в систему, которое скрывает панель входа для определенного игрока). triggerClientEvent позволяет вам указать получатель события в качестве первого (необязательного) аргумента. По умолчанию используется значение root, что означает, что если вы его не укажете, оно будет отправлено всем, кто подключен к серверу, даже тем, кто все еще загружает серверный кэш (что приводит к "событию, инициированному сервером на стороне клиента, eventName, но событие не добавлено на стороне клиента"). Вы можете передать либо элемент игрока, либо таблицу с получателями:
local playersToReceiveEvent = {player1, player2, player3} -- каждый PlayerX является элементом игрока
triggerClientEvent(playersToReceiveEvent, ...) -- не забудьте заполнить последнюю часть аргументов
Пример функции защиты от мошенничества, разработанной для событий, используемых для проверки данных как для обычных, так и для административных событий, которые вызываются со стороны клиента.
--[[
Для максимальной безопасности установите punishPlayerOnDetect, punishmentBan в true (как настроено по умолчанию)
Структура таблицы античита (processServerEventData) и её параметры:
checkACLGroup = { -- проверить, принадлежит ли игрок, вызвавший событие, хотя бы к одной из групп ниже, установите false/nil, чтобы не проверять это
"Admin",
},
checkPermissions = { -- проверить, есть ли у игрока, вызвавшего событие, разрешение хотя бы на одно из нижеперечисленного, установите false/nil, чтобы не проверять это
"function.kickPlayer",
},
checkEventData = {
{
debugData = "source", -- дополнительные данные для отчета, показанные в сообщении отладки
eventData = source, -- данные, которые мы хотим проверить
equalTo = client, -- сравнить, совпадают ли eventData и equalTo
allowedElements = { -- ограничить eventData определенными типами элементов, установите false/nil или оставьте пустым, чтобы не проверять это (полный список типов элементов: https://wiki.multitheftauto.com/wiki/GetElementsByType)
["player"] = true,
["ped"] = true,
["vehicle"] = true,
["object"] = true,
},
allowedDataTypes = { -- ограничить eventData определенными типами значений, установите false/nil или оставьте пустым, чтобы не проверять это
["string"] = true,
["number"] = true,
["table"] = true,
["boolean"] = true,
["nil"] = true,
},
allowedStringLength = {1, 32}, -- если eventData является строкой, то её длина должна быть в пределах (мин-макс), установите false/nil, чтобы не проверять длину строки - учтите, что allowedDataTypes должен содержать ["string"] = true
allowedTableLength = {1, 64}, -- если eventData является таблицей, то её длина должна быть в пределах (мин-макс), установите false/nil, чтобы не проверять длину таблицы - учтите, что allowedDataTypes должен содержать ["table"] = true
allowedNumberRange = {1, 128}, -- если eventData является числом, то оно должно быть в пределах (мин-макс), установите false/nil, чтобы не проверять диапазон чисел - учтите, что allowedDataTypes должен содержать ["number"] = true
},
},
]]
local punishPlayerOnDetect = true -- должен ли игрок быть наказан при обнаружении (убедитесь, что ресурс, запускающий этот код, имеет права администратора)
local punishmentBan = true -- актуально только если punishPlayerOnDetect установлен в true; используйте true для бана или false для кика
local punishmentReason = "Altering server event data" -- актуально только если punishPlayerOnDetect установлен в true; причина, которая будет показана наказанному игроку
local punishedBy = "Console" -- актуально только если punishPlayerOnDetect установлен в true; кто ответственен за наказание, также показывается наказанному игроку
local banByIP = false -- актуально только если punishPlayerOnDetect и punishmentBan установлены в true; бан по IP в настоящее время не рекомендуется (...)
local banByUsername = false -- имя пользователя сообщества - устаревшая вещь, поэтому установлено в false и должно оставаться таким
local banBySerial = true -- актуально только если punishPlayerOnDetect и punishmentBan установлены в true; (...) если есть серийный номер игрока, который можно использовать вместо этого
local banTime = 0 -- актуально только если punishPlayerOnDetect и punishmentBan установлены в true; время в секундах, 0 навсегда
local debugLevel = 4 -- этот уровень отладки позволяет скрыть префикс INFO: и использовать пользовательские цвета
local debugR = 255 -- сообщение отладки - красный цвет
local debugG = 127 -- сообщение отладки - зеленый цвет
local debugB = 0 -- сообщение отладки - синий цвет
function processServerEventData(clientElement, sourceElement, serverEvent, securityChecks) -- сердце нашего античита, которое выполняет все магические меры безопасности, возвращает true, если не было изменений от клиента (на основе данных из securityChecks), иначе false
if (not securityChecks) then -- если мы не передали никаких проверок безопасности
return true -- нечего проверять, даем коду идти дальше
end
if (not clientElement) then -- если переменная client недоступна по какой-то причине (хотя это не должно произойти)
local failReason = "Client variable not present" -- причина, показанная в отчете
local skipPunishment = true -- должен ли сервер пропустить наказание
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
local checkACLGroup = securityChecks.checkACLGroup -- если есть какие-либо группы ACL для проверки
local checkPermissions = securityChecks.checkPermissions -- если есть какие-либо разрешения для проверки
local checkEventData = securityChecks.checkEventData -- если есть какие-либо проверки данных
if (checkACLGroup) then -- проверим группы ACL игрока
local playerAccount = getPlayerAccount(clientElement) -- получить текущий аккаунт игрока
local guestAccount = isGuestAccount(playerAccount) -- если аккаунт гость (это значит, что игрок не вошел в систему)
if (guestAccount) then -- это так
local failReason = "Can't retrieve player login - guest account" -- причина, показанная в отчете
local skipPunishment = true -- должен ли сервер пропустить наказание
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
local accountName = getAccountName(playerAccount) -- получить имя текущего аккаунта игрока
local aclString = "user."..accountName -- форматируем это для дальнейшего использования в функции isObjectInACLGroup
for groupID = 1, #checkACLGroup do -- перебираем таблицу заданных групп
local groupName = checkACLGroup[groupID] -- получить имя каждой группы
local aclGroup = aclGetGroup(groupName) -- проверить, существует ли такая группа
if (not aclGroup) then -- не существует
local failReason = "ACL group '"..groupName.."' is missing" -- причина, показанная в отчете
local skipPunishment = true -- должен ли сервер пропустить наказание
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
local playerInACLGroup = isObjectInACLGroup(aclString, aclGroup) -- проверить, принадлежит ли игрок к группе
if (playerInACLGroup) then -- да, это так
return true -- значит, успех
end
end
local failReason = "Player doesn't belong to any given ACL group" -- причина, показанная в отчете
local skipPunishment = true -- должен ли сервер пропустить наказание
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
if (checkPermissions) then -- проверить, есть ли у игрока хотя бы одно желаемое разрешение
local allowedByDefault = false -- есть ли у него доступ по умолчанию
for permissionID = 1, #checkPermissions do -- перебираем все разрешения
local permissionName = checkPermissions[permissionID] -- получить имя разрешения
local hasPermission = hasObjectPermissionTo(clientElement, permissionName, allowedByDefault) -- проверить, разрешено ли игроку выполнять определенное действие
if (hasPermission) then -- если у игрока есть доступ
return true -- одно доступно (и достаточно), сервер не станет проверять остальные (так как return также прерывает цикл)
end
end
local failReason = "Not enough permissions" -- причина, показанная в отчете
local skipPunishment = true -- должен ли сервер пропустить наказание
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
if (checkEventData) then -- если есть какие-то данные для проверки
for dataID = 1, #checkEventData do -- перебираем каждые данные
local dataToCheck = checkEventData[dataID] -- получить каждый набор данных
local eventData = dataToCheck.eventData -- это то, что мы будем проверять
local equalTo = dataToCheck.equalTo -- мы хотим сравнить, совпадают ли eventData и equalTo
local allowedElements = dataToCheck.allowedElements -- проверить, является ли это элементом и принадлежит ли к определенным типам элементов
local allowedDataTypes = dataToCheck.allowedDataTypes -- ограничиваем ли мы данные определенным типом?
local debugData = dataToCheck.debugData -- дополнительные вспомогательные данные
local debugText = debugData and " ("..debugData..")" or "" -- если оно присутствует, красиво форматируем
if (equalTo) then -- проверка на равенство существует
local matchingData = (eventData == equalTo) -- сравнить, равны ли эти два значения
if (not matchingData) then -- они не равны
local failReason = "Data isn't equal @ argument "..dataID..debugText -- причина, показанная в отчете
local skipPunishment = false -- должен ли сервер пропустить наказание
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
end
if (allowedElements) then -- мы проверяем, является ли это элементом и принадлежит ли хотя бы одному из списка
local validElement = isElement(eventData) -- проверить, является ли это фактическим элементом
if (not validElement) then -- это не так
local failReason = "Data isn't element @ argument "..dataID..debugText -- причина, показанная в отчете
local skipPunishment = false -- должен ли сервер пропустить наказание
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
local elementType = getElementType(eventData) -- это элемент, поэтому мы хотим узнать его тип
local matchingElementType = allowedElements[elementType] -- проверить, разрешено ли это
if (not matchingElementType) then -- не разрешено
local failReason = "Invalid element type @ argument "..dataID..debugText -- причина, показанная в отчете
local skipPunishment = false -- должен ли сервер пропустить наказание
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
end
if (allowedDataTypes) then -- проверим разрешенные типы данных
local dataType = type(eventData) -- получить тип данных
local matchingType = allowedDataTypes[dataType] -- проверить, разрешено ли это
if (not matchingType) then -- не разрешено
local failReason = "Invalid data type @ argument "..dataID..debugText -- причина, показанная в отчете
local skipPunishment = false -- должен ли сервер пропустить наказание
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
local allowedStringLength = dataToCheck.allowedStringLength -- если данные имеют проверку длины строки
local dataString = (dataType == "string") -- убеждаемся, что это строка
if (allowedStringLength and dataString) then -- если мы должны проверить длину строки
local minLength = allowedStringLength[1] -- получить минимальную длину
local maxLength = allowedStringLength[2] -- получить максимальную длину
local stringLength = utf8.len(eventData) -- получить длину строки данных
local matchingLength = (stringLength >= minLength) and (stringLength <= maxLength) -- сравниваем, подходит ли значение в промежуток
if (not matchingLength) then -- если не подходит
local failReason = "Invalid string length (must be between "..minLength.."-"..maxLength..") @ argument "..dataID..debugText -- причина, показанная в отчете
local skipPunishment = false -- должен ли сервер пропустить наказание
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
end
local allowedTableLength = dataToCheck.allowedTableLength -- если данные имеют проверку длины таблицы
local dataTable = (dataType == "table") -- убеждаемся, что это таблица
if (allowedTableLength and dataTable) then -- если мы должны проверить длину таблицы
local minLength = allowedTableLength[1] -- получить минимальную длину
local maxLength = allowedTableLength[2] -- получить максимальную длину
local minLengthAchieved = false -- переменная, которая проверяет 'была ли достигнута минимальная длина'
local maxLengthExceeded = false -- переменная, которая проверяет 'превысила ли длина разрешенный максимум'
local tableLength = 0 -- сохранить начальную длину таблицы
for _, _ in pairs(eventData) do -- проходим по всей таблице
tableLength = (tableLength + 1) -- добавляем + 1 для каждой записи таблицы
minLengthAchieved = (tableLength >= minLength) -- больше ли длина или по крайней мере такой минимум, который мы требуем
maxLengthExceeded = (tableLength > maxLength) -- превысила ли таблица максимальную длину?
if (maxLengthExceeded) then -- она больше, чем должна быть
break -- прерываем цикл (из-за того, что условие выше выполнено, нет смысла считать дальше и тратить процессор на таблицу, которая потенциально может иметь огромное количество записей)
end
end
local matchingLength = (minLengthAchieved and not maxLengthExceeded) -- проверяем, достигнута ли минимальная длина, и убеждаемся, что она не превышает максимальную
if (not matchingLength) then -- эта таблица не соответствует требованиям
local failReason = "Invalid table length (must be between "..minLength.."-"..maxLength..") @ argument "..dataID..debugText -- причина, показанная в отчете
local skipPunishment = false -- должен ли сервер пропустить наказание
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
end
local allowedNumberRange = dataToCheck.allowedNumberRange -- если данные имеют проверку диапазона чисел
local dataNumber = (dataType == "number") -- убеждаемся, что это число
if (allowedNumberRange and dataNumber) then -- если мы должны проверить диапазон чисел
local minRange = allowedNumberRange[1] -- получить минимальный диапазон чисел
local maxRange = allowedNumberRange[2] -- получить максимальный диапазон чисел
local matchingRange = (eventData >= minRange) and (eventData <= maxRange) -- сравниваем, подходит ли значение в промежуток
if (not matchingRange) then -- если не подходит
local failReason = "Invalid number range (must be between "..minRange.."-"..maxRange..") @ argument "..dataID..debugText -- причина, показанная в отчете
local skipPunishment = false -- должен ли сервер пропустить наказание
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- сообщить о происшествии и обработать (или нет) этого игрока
return false -- вернуть неудачу
end
end
end
end
end
return true -- проверки безопасности пройдены, с этим вызовом события всё в порядке
end
function reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- вспомогательная функция для логирования и обработки инцидентов
local logClient = inspect(clientElement) -- подробный вид игрока, вызвавшего событие
local logSerial = getPlayerSerial(clientElement) or "N/A" -- серийник клиента, или "N/A", если по какой-то причине это невозможно
local logSource = inspect(sourceElement) -- подробный вид исходного элемента
local logText = -- заполняем наш отчет данными
"*\n"..
"Detected event abnormality:\n"..
"Client: "..logClient.."\n"..
"Client serial: "..logSerial.."\n"..
"Source: "..logSource.."\n"..
"Event: "..serverEvent.."\n"..
"Reason: "..failReason.."\n"..
"*"
outputDebugString(logText, debugLevel, debugR, debugG, debugB) -- выводим это в отладку
if (not punishPlayerOnDetect or skipPunishment) then -- мы не хотим наказывать игрока по какой-то причине
return true -- останавливаемся здесь
end
if (punishmentBan) then -- если это бан
banPlayer(clientElement, banByIP, banByUsername, banBySerial, punishedBy, punishmentReason, banTime) -- удаляем его присутствие с сервера
else -- иначе
kickPlayer(clientElement, punishedBy, punishmentReason) -- просто кикаем игрока с сервера
end
return true -- всё готово, сообщаем об успехе
end
function onServerEvent(clientData)
--[[
Предположим, что это серверское событие (функция) вызывается следующим образом:
local dataToPass = 10
triggerServerEvent("onServerEvent", localPlayer, dataToPass)
]]
local shouldProcessServerCode = processServerEventData(
client, -- элемент клиента - ответственный за вызов события
source, -- исходный элемент - переданный в triggerServerEvent (в качестве 2-го аргумента)
eventName, -- имя события - в данном случае 'onServerEvent'
{
checkEventData = { -- мы хотим проверить всё, что приходит от клиента
{
eventData = source, -- первое на проверку, переменная source
equalTo = client, -- мы хотим проверить, совпадает ли это с игроком, вызвавшим событие
debugData = "source", -- вспомогательные данные, которые будут показаны в отчете отладки
},
{
eventData = clientData, -- давайте проверим данные, которые клиент прислал нам
allowedDataTypes = {
["number"] = true, -- мы хотим, чтобы это было только число
},
allowedNumberRange = {1, 100}, -- в диапазоне от 1 до 100
debugData = "clientData", -- если что-то пойдет не так, дадим серверу знать где (это появится в отчете отладки)
},
},
}
)
if (not shouldProcessServerCode) then -- что-то не так, зеленого света для выполнения кода за этим пределом нет
return false -- остановить выполнение кода
end
-- выполняем код как обычно
end
addEvent("onServerEvent", true)
addEventHandler("onServerEvent", root, onServerEvent)
function onServerAdminEvent(playerToBan)
--[[
Предположим, что это серверское событие администратора (функция) вызывается следующим образом:
local playerToBan = getPlayerFromName("playerToBan")
triggerServerEvent("onServerAdminEvent", localPlayer, playerToBan)
]]
local shouldProcessServerCode = processServerEventData(
client, -- элемент клиента - ответственный за вызов события
source, -- исходный элемент - переданный в triggerServerEvent (в качестве 2-го аргумента)
eventName, -- имя события - в данном случае 'onServerAdminEvent'
{
checkACLGroup = { -- нам нужно проверить, принадлежит ли игрок, вызвавший событие, к группам ACL
"Admin", -- в данном случае группа администраторов
},
checkEventData = { -- мы хотим проверить всё, что приходит от клиента
{
eventData = source, -- первое на проверку, переменная source
equalTo = client, -- мы хотим проверить, совпадает ли это с игроком, вызвавшим событие
debugData = "source", -- вспомогательные данные, которые будут показаны в отчете отладки
},
{
eventData = playerToBan, -- давайте проверим данные, которые клиент прислал нам
allowedDataTypes = {
["player"] = true, -- мы хотим, чтобы это был игрок
},
debugData = "playerToBan", -- если что-то пойдет не так, дадим серверу знать где (это появится в отчете отладки)
},
},
}
)
if (not shouldProcessServerCode) then -- что-то не так, зеленого света для выполнения кода за этим пределом нет
return false -- остановить выполнение кода
end
-- выполняем код как обычно
end
addEvent("onServerAdminEvent", true)
addEventHandler("onServerAdminEvent", root, onServerAdminEvent)
Защита событий, относящихся только к серверу
- Очень важно отключить удаленный запуск в addEvent, чтобы предотвратить вызов событий, относящихся только к серверу, со стороны клиента.
- События в стиле Admin должны подтверждать права доступа игрока, либо с помощью isObjectInACLGroup, либо hasObjectPermissionTo.
В этом примере показано, как вы должны создавать события только на стороне сервера.
function onServerSideOnlyEvent()
-- делаем какие-то вещи на стороне сервера
end
addEvent("onServerSideOnlyEvent", false) -- установите второй аргумент (allowRemoteTriger) в false, чтобы его нельзя было вызвать с клиента
addEventHandler("onServerSideOnlyEvent", root, onServerSideOnlyEvent) -- связываем наше событие с функцией onServerSideOnlyEvent
Защита снарядов и взрывных устройств
Этот раздел (и код - работа продолжается) - используйте на свой страх и риск.
Обработка незарегистрированных событий
Смотрите: onPlayerTriggerInvalidEvent
Обработка нежелательных событий
Смотрите: onPlayerTriggerEventThreshold