User:DColeman/RU:Безопасность скриптов: Difference between revisions
| Line 45: | Line 45: | ||
* При просмотре скриптов на наличие дыр безопасности, сверяйтесь с данными, поступающими от клиента, которому можно доверять. | * При просмотре скриптов на наличие дыр безопасности, сверяйтесь с данными, поступающими от клиента, которому можно доверять. | ||
* Отправлены могут быть любые данные, поэтому любые серверные скрипты, которые коммуницируют с клиентскими и получают информацию от игроков, '''должны проверять информацию на корректность''' перед дальнейшим использованием. Подделка данных чаще всего происходит с помощью [[setElementData]] и [[triggerServerEvent]]. | * Отправлены могут быть любые данные, поэтому любые серверные скрипты, которые коммуницируют с клиентскими и получают информацию от игроков, '''должны проверять информацию на корректность''' перед дальнейшим использованием. Подделка данных чаще всего происходит с помощью [[setElementData]] и [[triggerServerEvent]]. | ||
* Вы не должны полагаться только на [https://wiki.multitheftauto.com/wiki/Serial серийный номер] игрока, когда | * Вы не должны полагаться только на [https://wiki.multitheftauto.com/wiki/Serial серийный номер] игрока, когда он используется для критических действий (авто-входа/администраторских действий). '''Не гарантируется, что серийный номер игрока уникален и не подделан'''. Это причина, почему вы должны '''поместить его за''' системой аккаунтов, в качестве '''важного аутентификационного фактора'''. | ||
* Серверная логика '''не может быть обойдена''' (если только сервер не взломан или в коде нет ошибки, но это совсем другой сценарий.) - '''используйте это в своих интересах'''. В большинстве случаев, вы можете реализовать проверки безопасности только на серверной стороне, не привлекая клиент. | * Серверная логика '''не может быть обойдена''' (если только сервер не взломан или в коде нет ошибки, но это совсем другой сценарий.) - '''используйте это в своих интересах'''. В большинстве случаев, вы можете реализовать проверки безопасности только на серверной стороне, не привлекая клиент. | ||
* Следование аксиоме '''''“Все параметры, включая source могут быть подделаны и им нельзя доверять. Глобальной переменной client можно доверять.”''''' дает вам уверенность в том, что игрок, к которому вы обращаетесь (через переменную '''client''') '''именно тот, кто вызвал событие'''. Это защищает вас от тех ситуаций, когда читер может вызывать, например, админские события (например, кик/бан игрока), указывая реального администратора ('''второй''' аргумент '''[[triggerServerEvent]]'''), или вызывать события за других игроков (будто они вызвали их, но в реальности это сделал читер) - всего лишь из-за использования некорректной переменной. Чтобы убедиться, что вы это поняли, обратимся к коду: | * Следование аксиоме '''''“Все параметры, включая source могут быть подделаны и им нельзя доверять. Глобальной переменной client можно доверять.”''''' дает вам уверенность в том, что игрок, к которому вы обращаетесь (через переменную '''client''') '''именно тот, кто вызвал событие'''. Это защищает вас от тех ситуаций, когда читер может вызывать, например, админские события (например, кик/бан игрока), указывая реального администратора ('''второй''' аргумент '''[[triggerServerEvent]]'''), или вызывать события за других игроков (будто они вызвали их, но в реальности это сделал читер) - всего лишь из-за использования некорректной переменной. Чтобы убедиться, что вы это поняли, обратимся к коду: | ||
Latest revision as of 14:26, 23 May 2025
Информация по памяти клиента
Начиная с самых основ:
- Вы должны понимать, что все, что вы храните на стороне клиента, включая .lua файлы, находится под риском. Любая конфиденциальная или критическая информация, которая как-либо оказывается на клиентской стороне (ПК игрока), может быть прочитана и/или изменена чит-клиентами.
- Чтобы сохранить конфиденциальную информацию и логику скрипта в секрете - используйте серверную сторону.
- Обратите внимание, что скрипты, отмеченные как общие (shared) также действуют как клиентские, что означает, что все вышеперечисленное также применяется и к ним. Например, объявление скрипта:
<script src="script.lua" type="shared"/> <!-- этот скрипт будет запущен и на сервере и на клиенте -->
То же самое, что и объявление:
<script src="script.lua" type="client"/> <!-- объявляем скрипт на клиенте --> <script src="script.lua" type="server"/> <!-- делаем то же самое, но на клиенте -->
Дополнительный слой защиты
Чтобы значительно усложнить жизнь* тем, у кого есть плохие намерения по отношению к вашему серверу, вы можете использовать аттрибут cache (и/или скомпилированные lua скрипты (также известные как Luac) с 3 уровнем дополнительной обфускации - API), доступный в meta.xml, вместе с настройкой встроенного античита МТА с включением SD (специальными кодами обнаружения), для дополнительной информации смотрите гайд по античиту.
<script src="shared.lua" type="shared" cache="false"/> <!-- cache="false" означает, что этот Lua файл не будет сохранен на ПК игрока --> <script src="client.lua" type="client" cache="false"/> <!-- cache="false" означает, что этот Lua файл не будет сохранен на ПК игрока -->
- Усложнить жизнь не означает "сделать невозможным" получение вашего клиентского Lua кода, это означает, что большинство людей не смогут просмотреть ваши .lua файлы - тех, кто ищет логические ошибки (баги) или отсутствующие/некорректные проверки безопасности.
- Может использоваться на client и shared типах скриптов (не имеет эффекта на серверных скриптах).
- Это не удалит Lua файлы, которые были загружены ранее.
Обнаружение и обезвреживание бэкдоров и читов
Чтобы минимизировать (или исключить) урон, причиненный Lua скриптами:
- Всегда обновляйте ваш сервер до самой актуальной версии, вы можете загрузить новейшие сборки сервера отсюда. С информацией по определенным сборкам можно ознакомиться здесь.
- Всегда обновляйте ресурсы, установленные на сервере, до самой актуальной версии, вы можете загрузить новейшие (стандартные) ресурсы из репозитория GitHub. Они часто содержат обновления безопасности, которые влияют на устойчивость вашего сервера к атакам.
- Убедитесь, что ACL (список контроля доступа) правильно сконфигурирован - он поможет заблокировать ресурсам некоторые потенциально опасные функции.
- Никогда не выдавайте админ-права ресурсам (включая карты), полученным из неизвестных источников.
- Перед запуском недоверенного ресурса, изучите:
- Его meta.xml, на наличие возможных скрытых скриптов, которые скрываются под видом файлов с другими расширениями.
- Его исходный код, на наличие вредоносной логики.
- Не запускайте и не используйте скомпилированные ресурсы (скрипты), в легитимности которых вы не уверены.
Чтобы минимизировать урон, причиненный читером, зашедшим на сервер:
- При создании скриптов никогда не доверяйте данным, полученным со стороны клиента.
- При просмотре скриптов на наличие дыр безопасности, сверяйтесь с данными, поступающими от клиента, которому можно доверять.
- Отправлены могут быть любые данные, поэтому любые серверные скрипты, которые коммуницируют с клиентскими и получают информацию от игроков, должны проверять информацию на корректность перед дальнейшим использованием. Подделка данных чаще всего происходит с помощью setElementData и triggerServerEvent.
- Вы не должны полагаться только на серийный номер игрока, когда он используется для критических действий (авто-входа/администраторских действий). Не гарантируется, что серийный номер игрока уникален и не подделан. Это причина, почему вы должны поместить его за системой аккаунтов, в качестве важного аутентификационного фактора.
- Серверная логика не может быть обойдена (если только сервер не взломан или в коде нет ошибки, но это совсем другой сценарий.) - используйте это в своих интересах. В большинстве случаев, вы можете реализовать проверки безопасности только на серверной стороне, не привлекая клиент.
- Следование аксиоме “Все параметры, включая source могут быть подделаны и им нельзя доверять. Глобальной переменной client можно доверять.” дает вам уверенность в том, что игрок, к которому вы обращаетесь (через переменную client) именно тот, кто вызвал событие. Это защищает вас от тех ситуаций, когда читер может вызывать, например, админские события (например, кик/бан игрока), указывая реального администратора (второй аргумент triggerServerEvent), или вызывать события за других игроков (будто они вызвали их, но в реальности это сделал читер) - всего лишь из-за использования некорректной переменной. Чтобы убедиться, что вы это поняли, обратимся к коду:
--[[
НИКОГДА НЕ ИСПОЛЬЗУЙТЕ ЭТОТ КОД - ОН АБСОЛЮТНО НЕВЕРНЫЙ И НЕБЕЗОПАСНЫЙ
ПРОБЛЕМА: ИСПОЛЬЗУЯ ПЕРЕМЕННУЮ 'source' В hasObjectPermissionTo ВЫ ОСТАВЛЯЕТЕ ДЫРУ ДЛЯ ЧИТЕРОВ
]]
function onServerWrongAdminEvent(playerToBan)
if (not client) then -- 'client' указывает на игрока, который вызвал событие, и должен использоваться в качестве проверки безопасности (для предотвращения подмены игрока)
return false -- если эта переменная не существует (по неизвестной причине), останавливаем выполнение кода
end
local defaultPermission = false -- по стандарту не разрешаем действие, сморите: 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 -- если нет...
return false -- останавливаем выполнение кода
end
local elementType = getElementType(playerToBan) -- это элемент, получаем его тип
local playerType = (elementType == "player") -- проверяем, что тип элемента - игрок
if (not playerType) then -- это не игрок
return false -- останавливаемся
end
-- banPlayer(...) -- делаем, что нужно
end
addEvent("onServerWrongAdminEvent", true)
addEventHandler("onServerWrongAdminEvent", root, onServerWrongAdminEvent)
--[[
onServerCorrectAdminEvent ПРЕКРАСНО ЗАЩИЩЕН, ТАК, КАК ДОЛЖНО БЫТЬ
ЗДЕСЬ НЕТ ПРОБЛЕМ: МЫ ИСПОЛЬЗУЕМ 'client' В hasObjectPermissionTo, ЧТО ЗАЩИЩАЕТ НАС ОТ ПОДМЕНЫ ИГРОКА, КОТОРЫЙ ВЫЗВАЛ СОБЫТИЕ
]]--
function onServerCorrectAdminEvent(playerToBan)
if (not client) then -- 'client' указывает на игрока, который вызвал событие, и должен использоваться в качестве проверки безопасности (для предотвращения подмены игрока)
return false -- если эта переменная не существует (по неизвестной причине), останавливаем выполнение кода
end
local defaultPermission = false -- по стандарту не разрешаем действие, сморите: https://wiki.multitheftauto.com/wiki/HasObjectPermissionTo
local canAdminBanPlayer = hasObjectPermissionTo(client, "function.banPlayer", defaultPermission) -- если игрок имеет права
if (not canAdminBanPlayer) then -- если у игрока нет прав
return false -- останавливаем выполнение кода
end
local validElement = isElement(playerToBan) -- проверяем, что аргумент, переданный игроком, является элементом
if (not validElement) then -- если нет...
return false -- останавливаем выполнение кода
end
local elementType = getElementType(playerToBan) -- это элемент, получаем его тип
local playerType = (elementType == "player") -- проверяем, что тип элемента - игрок
if (not playerType) then -- это не игрок
return false -- останавливаемся
end
-- banPlayer(...) -- делаем, что нужно
end
addEvent("onServerCorrectAdminEvent", true)
addEventHandler("onServerCorrectAdminEvent", root, onServerCorrectAdminEvent)
Securing setElementData
- You should refrain from using element data everywhere, it should be only used when really necessary. It is advised to replace it with triggerClientEvent instead.
- If you still insist on using it, it is recommended to set 4th argument in setElementData to false (disabling sync for this, certain data) and use subscriber mode - addElementDataSubscriber, for both security & performance reasons described in event section.
| Important Note: Disabling sync however, doesn't fully protect data key. It would be still vulnerable by default, but in this case you are not leaving it in plain sight. Using getAllElementData or digging in RAM wouldn't expose it, since it won't be synced to client in first place. Therefore, it needs to be added to anti-cheat as well. |
- All parameters including source can be faked and should not be trusted.
- Global variable client can be trusted.
Example of basic element data anti-cheat.
local function reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue) -- helper function to log and revert changes
local logClient = inspect(clientElement) -- in-depth view of player which forced element data sync
local logSerial = getPlayerSerial(clientElement) or "N/A" -- client serial, or "N/A" if not possible, for some reason
local logSource = tostring(sourceElement) -- element which received data
local logOldValue = tostring(oldValue) -- old value
local logNewValue = tostring(newValue) -- new value
local logText = -- fill our report with data
"=======================================\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 -- specify who will see this log in console, in this case each player connected to server
local hadData = (oldValue ~= nil) -- check if element had such data before
if (hadData) then -- if element had such data before
setElementData(sourceElement, dataKey, oldValue, true) -- revert changes, it will call onElementDataChange event, but will fail (stop) on first condition - because server (not client) forced change
else
removeElementData(sourceElement, dataKey) -- remove it completely
end
outputConsole(logText, logVisibleTo) -- print it to console
return true -- all success
end
function onElementDataChangeBasicAC(dataKey, oldValue, newValue) -- the heart of our anti-cheat, which does all the magic security measurements
if (not client) then -- check if data is coming from client
return false -- if it's not, do not go further
end
local checkSpecialThing = (dataKey == "special_thing") -- compare whether dataKey matches "special_thing"
local checkFlagWaving = (dataKey == "flag_waving") -- compare whether dataKey matches "flag_waving"
if (checkSpecialThing) then -- if it does, do our security checks
local invalidElement = (client ~= source) -- verify whether source element is different from player which changed data
if (invalidElement) then -- if it's so
reportAndRevertDataChange(client, source, dataKey, oldValue, newValue) -- revert data change, because "special_thing" can only be set for player himself
end
end
if (checkFlagWaving) then -- if it does, do our security checks
local playerVehicle = getPedOccupiedVehicle(client) -- get player's current vehicle
local invalidVehicle = (playerVehicle ~= source) -- verify whether source element is different from player's vehicle
if (invalidVehicle) then -- if it's so
reportAndRevertDataChange(client, source, dataKey, oldValue, newValue) -- revert data change, because "flag_waving" can only be set for player's own vehicle
end
end
end
addEventHandler("onElementDataChange", root, onElementDataChangeBasicAC)
Example of advanced element data anti-cheat.
--[[
For maximum security set punishPlayerOnDetect, punishmentBan, allowOnlyProtectedKeys to true (as per default configuration)
If allowOnlyProtectedKeys is enabled, do not forget to add every client-side element data key to protectedKeys table - otherwise you will face false-positives
Anti-cheat (handleDataChange) table structure and it's options:
["keyName"] = { -- name of key which would be protected
onlyForPlayerHimself = true, -- enabling this (true) will make sure that this element data key can only be set on player who synced it (ignores onlyForOwnPlayerVeh and allowForElements), use false/nil to disable this
onlyForOwnPlayerVeh = false, -- enabling this (true) will make sure that this element data key can only be set on player's current vehicle who synced it (ignores allowForElements), use false/nil to disable this
allowForElements = { -- restrict this key for certain element type(s), set to false/nil or leave it empty to not check this (full list of element types: https://wiki.multitheftauto.com/wiki/GetElementsByType)
["player"] = true,
["ped"] = true,
["vehicle"] = true,
["object"] = true,
},
allowedDataTypes = { -- restrict this key for certain value type(s), set to false/nil or leave it empty to not check this
["string"] = true,
["number"] = true,
["table"] = true,
["boolean"] = true,
["nil"] = true,
},
allowedStringLength = {1, 32}, -- if value is a string, then it's length must be in between (min-max), set it to false/nil to not check string length - do note that allowedDataTypes must contain ["string"] = true
allowedTableLength = {1, 64}, -- if value is a table, then it's length must be in between (min-max), set it to false/nil to not check table length - do note that allowedDataTypes must contain ["table"] = true
allowedNumberRange = {1, 128}, -- if value is a number, then it must be in between (min-max), set it to false/nil to not check number range - do note that allowedDataTypes must contain ["number"] = true
}
]]
local punishPlayerOnDetect = true -- should player be punished upon detection (make sure that resource which runs this code has admin rights)
local punishmentBan = true -- only relevant if punishPlayerOnDetect is set to true; use true for ban or false for kick
local punishmentReason = "Altering element data" -- only relevant if punishPlayerOnDetect is set to true; reason which would be shown to punished player
local punishedBy = "Console" -- only relevant if punishPlayerOnDetect is set to true; who was responsible for punishing, as well shown to punished player
local banByIP = false -- only relevant if punishPlayerOnDetect and punishmentBan is set to true; banning by IP nowadays is not recommended (...)
local banByUsername = false -- community username - legacy thing, hence is set to false and should stay like that
local banBySerial = true -- only relevant if punishPlayerOnDetect and punishmentBan is set to true; (...) if there is a player serial to use instead
local banTime = 0 -- only relevant if punishPlayerOnDetect and punishmentBan is set to true; time in seconds, 0 for permanent
local allowOnlyProtectedKeys = true -- disallow (remove by using removeElementData) every element data besides those given in protectedKeys table; in case someone wanted to flood server with garbage keys, which would be kept in memory until server restart or manual remove - setElementData(source, key, nil) won't remove it; it has to be removeElementData
local debugLevel = 4 -- this debug level allows to hide INFO: prefix, and use custom colors
local debugR = 255 -- debug message - red color
local debugG = 127 -- debug message - green color
local debugB = 0 -- debug message - blue color
local protectedKeys = {
["vehicleNumber"] = { -- we want vehicleNumber to be set only on vehicles, with stricte numbers in range of 1-100
allowForElements = {
["vehicle"] = true,
},
allowedDataTypes = {
["number"] = true,
},
allowedNumberRange = {1, 100},
},
["personalVehData"] = { -- we want be able to set personalVehData only on current vehicle, also it should be a string with length between 1-24
onlyForOwnPlayerVeh = true,
allowedDataTypes = {
["string"] = true,
},
allowedStringLength = {1, 24},
},
-- perform security checks on keys stored in this table, in a convenient way
}
local function reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- helper function to log and revert changes
local logClient = inspect(clientElement) -- in-depth view of player which forced element data sync
local logSerial = getPlayerSerial(clientElement) or "N/A" -- client serial, or "N/A" if not possible, for some reason
local logSource = inspect(sourceElement) -- in-depth view of element which received data
local logOldValue = inspect(oldValue) -- in-depth view of old value
local logNewValue = inspect(newValue) -- in-depth view of new value
local logText = -- fill our report with data
"*\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) -- print it to debug
if (forceRemove) then -- we don't want this element data key to exist at all
removeElementData(sourceElement, dataKey) -- remove it
return true -- return success and stop here, we don't need further checks
end
setElementData(sourceElement, dataKey, oldValue, true) -- revert changes, it will call onElementDataChange event, but will fail (stop) on first condition - because server (not client) forced change
return true -- return success
end
local function handleDataChange(clientElement, sourceElement, dataKey, oldValue, newValue) -- the heart of our anti-cheat, which does all the magic security measurements, returns true if there was no altering from client (based on data from protectedKeys), false otherwise
local protectedKey = protectedKeys[dataKey] -- look up whether key changed is stored in protectedKeys table
if (not protectedKey) then -- if it's not
if (allowOnlyProtectedKeys) then -- if we don't want garbage keys
local failReason = "Key isn't present in protectedKeys" -- reason shown in report
local forceRemove = true -- should key be completely removed
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- report accident, and handle (or not) this player
return false -- return failure
end
return true -- this key isn't protected, let it through
end
local onlyForPlayerHimself = protectedKey.onlyForPlayerHimself -- if key has "self-set" lock
local onlyForOwnPlayerVeh = protectedKey.onlyForOwnPlayerVeh -- if key has "self-vehicle" lock
local allowForElements = protectedKey.allowForElements -- if key has element type check
local allowedDataTypes = protectedKey.allowedDataTypes -- if key has allowed data type check
if (onlyForPlayerHimself) then -- if "self-set" lock is active
local matchingElement = (clientElement == sourceElement) -- verify whether player who set data is equal to element which received data
if (not matchingElement) then -- if it's not matching
local failReason = "Can only set on player himself" -- reason shown in report
local forceRemove = false -- should key be completely removed
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- report accident, and handle (or not) this player
return false -- return failure
end
end
if (onlyForOwnPlayerVeh) then -- if "self-vehicle" lock is active
local playerVehicle = getPedOccupiedVehicle(clientElement) -- get current vehicle of player which set data
local matchingVehicle = (playerVehicle == sourceElement) -- check whether it matches the one which received data
if (not matchingVehicle) then -- if it doesn't match
local failReason = "Can only set on player's own vehicle" -- reason shown in report
local forceRemove = false -- should key be completely removed
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- report accident, and handle (or not) this player
return false -- return failure
end
end
if (allowForElements) then -- check if it's one of them
local elementType = getElementType(sourceElement) -- get type of element whose data changed
local matchingElementType = allowForElements[elementType] -- verify whether it's allowed
if (not matchingElementType) then -- this isn't matching
local failReason = "Invalid element type" -- reason shown in report
local forceRemove = false -- should key be completely removed
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- report accident, and handle (or not) this player
return false -- return failure
end
end
if (allowedDataTypes) then -- if there's allowed data types
local valueType = type(newValue) -- get data type of value
local matchingType = allowedDataTypes[valueType] -- check if it's one of allowed
if (not matchingType) then -- if it's not then
local failReason = "Invalid data type" -- reason shown in report
local forceRemove = false -- should key be completely removed
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- report accident, and handle (or not) this player
return false -- return failure
end
local allowedStringLength = protectedKey.allowedStringLength -- if key has specified string length check
local dataString = (valueType == "string") -- make sure it's a string
if (allowedStringLength and dataString) then -- if we should check string length
local minLength = allowedStringLength[1] -- retrieve min length
local maxLength = allowedStringLength[2] -- retrieve max length
local stringLength = utf8.len(newValue) -- get length of data string
local matchingLength = (stringLength >= minLength) and (stringLength <= maxLength) -- compare whether value fits in between
if (not matchingLength) then -- if it doesn't
local failReason = "Invalid string length (must be between "..minLength.."-"..maxLength..")" -- reason shown in report
local forceRemove = false -- should key be completely removed
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- report accident, and handle (or not) this player
return false -- return failure
end
end
local allowedTableLength = protectedKey.allowedTableLength -- if key has table length check
local dataTable = (valueType == "table") -- make sure it's a table
if (allowedTableLength and dataTable) then -- if we should check table length
local minLength = allowedTableLength[1] -- retrieve min length
local maxLength = allowedTableLength[2] -- retrieve max length
local minLengthAchieved = false -- variable which checks 'does minimum length was achieved'
local maxLengthExceeded = false -- variable which checks 'does length has exceeds more than allowed maximum'
local tableLength = 0 -- store initial table length
for _, _ in pairs(newValue) do -- loop through whole table
tableLength = (tableLength + 1) -- add + 1 on each table entry
minLengthAchieved = (tableLength >= minLength) -- is length bigger or at very minimum we require
maxLengthExceeded = (tableLength > maxLength) -- does table exceeded more than max length?
if (maxLengthExceeded) then -- it is bigger than it should be
break -- break the loop (due of condition above being worthy, it makes no point to count further and waste CPU, on a table which potentially could have huge amount of entries)
end
end
local matchingLength = (minLengthAchieved and not maxLengthExceeded) -- check if min length has been achieved, and make sure that it doesn't go beyond max length
if (not matchingLength) then -- this table doesn't match requirements
local failReason = "Invalid table length (must be between "..minLength.."-"..maxLength..")" -- reason shown in report
local forceRemove = false -- should key be completely removed
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- report accident, and handle (or not) this player
return false -- return failure
end
end
local allowedNumberRange = protectedKey.allowedNumberRange -- if key has allowed number range check
local dataNumber = (valueType == "number") -- make sure it's a number
if (allowedNumberRange and dataNumber) then -- if we should check number range
local minRange = allowedNumberRange[1] -- retrieve min number range
local maxRange = allowedNumberRange[2] -- retrieve max number range
local matchingRange = (newValue >= minRange) and (newValue <= maxRange) -- compare whether value fits in between
if (not matchingRange) then -- if it doesn't
local failReason = "Invalid number range (must be between "..minRange.."-"..maxRange..")" -- reason shown in report
local forceRemove = false -- should key be completely removed
reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue, failReason, forceRemove) -- report accident, and handle (or not) this player
return false -- return failure
end
end
end
return true -- security checks passed, we are all clear with this data key
end
function onElementDataChangeAdvancedAC(dataKey, oldValue, newValue) -- this event makes use of handleDataChange, the code was split for better readability
if (not client) then -- check if data is coming from client
return false -- if it's not, do not continue
end
local approvedChange = handleDataChange(client, source, dataKey, oldValue, newValue) -- run our security checks
if (approvedChange) then -- it's all cool and good
return false -- we don't need further action
end
if (not punishPlayerOnDetect) then -- we don't want to punish player for some reason
return false -- so stop here
end
if (punishmentBan) then -- if it's ban
banPlayer(client, banByIP, banByUsername, banBySerial, punishedBy, punishmentReason, banTime) -- remove his presence from server
else -- otherwise
kickPlayer(client, punishedBy, punishmentReason) -- simply kick player out of server
end
end
addEventHandler("onElementDataChange", root, onElementDataChangeAdvancedAC)
Securing triggerServerEvent
- All parameters including source can be faked and should not be trusted.
- Global variable client can be trusted.
- Admin styled events should be verifying player (client) ACL rights, either by isObjectInACLGroup or hasObjectPermissionTo.
- Do not use the same name for your custom event as MTA native server events (which aren't remotely triggerable by default), e.g: onPlayerLogin; doing so would open door for cheaters manipulations.
- Be aware to which players event is sent via triggerClientEvent. For both security & performance reasons, admin like events should be received by admins only (to prevent confidential data being accessed), in the same time you shouldn't send each event to everyone on server (e.g: login success event which hides login panel for certain player). triggerClientEvent allows you to specify the event receiver as first (optional) argument. It defaults to root, meaning if you don't specify it, it will be sent to everyone connected to server - even those who are still downloading server cache (which results in Server triggered clientside event eventName, but event is not added clientside.). You can either pass player element or table with receivers:
local playersToReceiveEvent = {player1, player2, player3} -- each playerX is player element
triggerClientEvent(playersToReceiveEvent, ...) -- do not forget to fill the latter part of arguments
Example of anti-cheat function designed for events, used for data validation for both normal, and admin events which are called from client-side.
--[[
For maximum security set punishPlayerOnDetect, punishmentBan to true (as per default configuration)
Anti-cheat (processServerEventData) table structure and it's options:
checkACLGroup = { -- check whether player who called event belongs to at least one group below, set to false/nil to not check this
"Admin",
},
checkPermissions = { -- check whether player who called event has permission to at least one thing below, set to false/nil to not check this
"function.kickPlayer",
},
checkEventData = {
{
debugData = "source", -- optional details for report shown in debug message
eventData = source, -- data we want to verify
equalTo = client, -- compare whether eventData == equalTo
allowedElements = { -- restrict eventData to be certain element type(s), set to false/nil or leave it empty to not check this (full list of element types: https://wiki.multitheftauto.com/wiki/GetElementsByType)
["player"] = true,
["ped"] = true,
["vehicle"] = true,
["object"] = true,
},
allowedDataTypes = { -- restrict eventData to be certain value type(s), set to false/nil or leave it empty to not check this
["string"] = true,
["number"] = true,
["table"] = true,
["boolean"] = true,
["nil"] = true,
},
allowedStringLength = {1, 32}, -- if eventData is a string, then it's length must be in between (min-max), set it to false/nil to not check string length - do note that allowedDataTypes must contain ["string"] = true
allowedTableLength = {1, 64}, -- if eventData is a table, then it's length must be in between (min-max), set it to false/nil to not check table length - do note that allowedDataTypes must contain ["table"] = true
allowedNumberRange = {1, 128}, -- if eventData is a number, then it must be in between (min-max), set it to false/nil to not check number range - do note that allowedDataTypes must contain ["number"] = true
},
},
]]
local punishPlayerOnDetect = true -- should player be punished upon detection (make sure that resource which runs this code has admin rights)
local punishmentBan = true -- only relevant if punishPlayerOnDetect is set to true; use true for ban or false for kick
local punishmentReason = "Altering server event data" -- only relevant if punishPlayerOnDetect is set to true; reason which would be shown to punished player
local punishedBy = "Console" -- only relevant if punishPlayerOnDetect is set to true; who was responsible for punishing, as well shown to punished player
local banByIP = false -- only relevant if punishPlayerOnDetect and punishmentBan is set to true; banning by IP nowadays is not recommended (...)
local banByUsername = false -- community username - legacy thing, hence is set to false and should stay like that
local banBySerial = true -- only relevant if punishPlayerOnDetect and punishmentBan is set to true; (...) if there is a player serial to use instead
local banTime = 0 -- only relevant if punishPlayerOnDetect and punishmentBan is set to true; time in seconds, 0 for permanent
local debugLevel = 4 -- this debug level allows to hide INFO: prefix, and use custom colors
local debugR = 255 -- debug message - red color
local debugG = 127 -- debug message - green color
local debugB = 0 -- debug message - blue color
function processServerEventData(clientElement, sourceElement, serverEvent, securityChecks) -- the heart of our anti-cheat, which does all the magic security measurements, returns true if there was no altering from client (based on data from securityChecks), false otherwise
if (not securityChecks) then -- if we haven't passed any security checks
return true -- nothing to check, let code go further
end
if (not clientElement) then -- if client variable isn't available for some reason (although it should never happen)
local failReason = "Client variable not present" -- reason shown in report
local skipPunishment = true -- should server skip punishment
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- report accident, and handle (or not) this player
return false -- return failure
end
local checkACLGroup = securityChecks.checkACLGroup -- if there's any ACL groups to check
local checkPermissions = securityChecks.checkPermissions -- if there's any permissions to check
local checkEventData = securityChecks.checkEventData -- if there's any data checks
if (checkACLGroup) then -- let's check player ACL groups
local playerAccount = getPlayerAccount(clientElement) -- get current account of player
local guestAccount = isGuestAccount(playerAccount) -- if account is guest (meaning player is not logged in)
if (guestAccount) then -- it's the case
local failReason = "Can't retrieve player login - guest account" -- reason shown in report
local skipPunishment = true -- should server skip punishment
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- report accident, and handle (or not) this player
return false -- return failure
end
local accountName = getAccountName(playerAccount) -- get name of player's current account
local aclString = "user."..accountName -- format it for further use in isObjectInACLGroup function
for groupID = 1, #checkACLGroup do -- iterate over table of given groups
local groupName = checkACLGroup[groupID] -- get each group name
local aclGroup = aclGetGroup(groupName) -- check if such group exists
if (not aclGroup) then -- it doesn't
local failReason = "ACL group '"..groupName.."' is missing" -- reason shown in report
local skipPunishment = true -- should server skip punishment
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- report accident, and handle (or not) this player
return false -- return failure
end
local playerInACLGroup = isObjectInACLGroup(aclString, aclGroup) -- check if player belong to the group
if (playerInACLGroup) then -- yep, it's the case
return true -- so it's a success
end
end
local failReason = "Player doesn't belong to any given ACL group" -- reason shown in report
local skipPunishment = true -- should server skip punishment
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- report accident, and handle (or not) this player
return false -- return failure
end
if (checkPermissions) then -- check if player has at least one desired permission
local allowedByDefault = false -- does he have access by default
for permissionID = 1, #checkPermissions do -- iterate over all permissions
local permissionName = checkPermissions[permissionID] -- get permission name
local hasPermission = hasObjectPermissionTo(clientElement, permissionName, allowedByDefault) -- check whether player is allowed to perform certain action
if (hasPermission) then -- if player has access
return true -- one is available (and enough), server won't bother to check others (as return keywords also breaks loop)
end
end
local failReason = "Not enough permissions" -- reason shown in report
local skipPunishment = true -- should server skip punishment
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- report accident, and handle (or not) this player
return false -- return failure
end
if (checkEventData) then -- if there is some data to verify
for dataID = 1, #checkEventData do -- iterate over each of data
local dataToCheck = checkEventData[dataID] -- get each data set
local eventData = dataToCheck.eventData -- this is the one we'll be verifying
local equalTo = dataToCheck.equalTo -- we want to compare whether eventData == equalTo
local allowedElements = dataToCheck.allowedElements -- check whether is element, and whether belongs to certain element types
local allowedDataTypes = dataToCheck.allowedDataTypes -- do we restrict data to be certain type?
local debugData = dataToCheck.debugData -- additional helper data
local debugText = debugData and " ("..debugData..")" or "" -- if it's present, format it nicely
if (equalTo) then -- equal check exists
local matchingData = (eventData == equalTo) -- compare whether those two values are equal
if (not matchingData) then -- they aren't
local failReason = "Data isn't equal @ argument "..dataID..debugText -- reason shown in report
local skipPunishment = false -- should server skip punishment
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- report accident, and handle (or not) this player
return false -- return failure
end
end
if (allowedElements) then -- we do check whether is an element, and belongs to at least one given in the list
local validElement = isElement(eventData) -- check if it's actual element
if (not validElement) then -- it's not
local failReason = "Data isn't element @ argument "..dataID..debugText -- reason shown in report
local skipPunishment = false -- should server skip punishment
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- report accident, and handle (or not) this player
return false -- return failure
end
local elementType = getElementType(eventData) -- it's element, so we want to know it's type
local matchingElementType = allowedElements[elementType] -- verify whether it's allowed
if (not matchingElementType) then -- it's not allowed
local failReason = "Invalid element type @ argument "..dataID..debugText -- reason shown in report
local skipPunishment = false -- should server skip punishment
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- report accident, and handle (or not) this player
return false -- return failure
end
end
if (allowedDataTypes) then -- let's check allowed data types
local dataType = type(eventData) -- get data type
local matchingType = allowedDataTypes[dataType] -- verify whether it's allowed
if (not matchingType) then -- it isn't
local failReason = "Invalid data type @ argument "..dataID..debugText -- reason shown in report
local skipPunishment = false -- should server skip punishment
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- report accident, and handle (or not) this player
return false -- return failure
end
local allowedStringLength = dataToCheck.allowedStringLength -- if data has string length check
local dataString = (dataType == "string") -- make sure it's a string
if (allowedStringLength and dataString) then -- if we should check string length
local minLength = allowedStringLength[1] -- retrieve min length
local maxLength = allowedStringLength[2] -- retrieve max length
local stringLength = utf8.len(eventData) -- get length of data string
local matchingLength = (stringLength >= minLength) and (stringLength <= maxLength) -- compare whether value fits in between
if (not matchingLength) then -- if it doesn't
local failReason = "Invalid string length (must be between "..minLength.."-"..maxLength..") @ argument "..dataID..debugText -- reason shown in report
local skipPunishment = false -- should server skip punishment
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- report accident, and handle (or not) this player
return false -- return failure
end
end
local allowedTableLength = dataToCheck.allowedTableLength -- if data has table length check
local dataTable = (dataType == "table") -- make sure it's a table
if (allowedTableLength and dataTable) then -- if we should check table length
local minLength = allowedTableLength[1] -- retrieve min length
local maxLength = allowedTableLength[2] -- retrieve max length
local minLengthAchieved = false -- variable which checks 'does minimum length was achieved'
local maxLengthExceeded = false -- variable which checks 'does length has exceeds more than allowed maximum'
local tableLength = 0 -- store initial table length
for _, _ in pairs(eventData) do -- loop through whole table
tableLength = (tableLength + 1) -- add + 1 on each table entry
minLengthAchieved = (tableLength >= minLength) -- is length bigger or at very minimum we require
maxLengthExceeded = (tableLength > maxLength) -- does table exceeded more than max length?
if (maxLengthExceeded) then -- it is bigger than it should be
break -- break the loop (due of condition above being worthy, it makes no point to count further and waste CPU, on a table which potentially could have huge amount of entries)
end
end
local matchingLength = (minLengthAchieved and not maxLengthExceeded) -- check if min length has been achieved, and make sure that it doesn't go beyond max length
if (not matchingLength) then -- this table doesn't match requirements
local failReason = "Invalid table length (must be between "..minLength.."-"..maxLength..") @ argument "..dataID..debugText -- reason shown in report
local skipPunishment = false -- should server skip punishment
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- report accident, and handle (or not) this player
return false -- return failure
end
end
local allowedNumberRange = dataToCheck.allowedNumberRange -- if data has number range check
local dataNumber = (dataType == "number") -- make sure it's a number
if (allowedNumberRange and dataNumber) then -- if we should check number range
local minRange = allowedNumberRange[1] -- retrieve min number range
local maxRange = allowedNumberRange[2] -- retrieve max number range
local matchingRange = (eventData >= minRange) and (eventData <= maxRange) -- compare whether value fits in between
if (not matchingRange) then -- if it doesn't
local failReason = "Invalid number range (must be between "..minRange.."-"..maxRange..") @ argument "..dataID..debugText -- reason shown in report
local skipPunishment = false -- should server skip punishment
reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- report accident, and handle (or not) this player
return false -- return failure
end
end
end
end
end
return true -- security checks passed, we are all clear with this event call
end
function reportAndHandleEventAbnormality(clientElement, sourceElement, serverEvent, failReason, skipPunishment) -- helper function to log and handle accidents
local logClient = inspect(clientElement) -- in-depth view player which called event
local logSerial = getPlayerSerial(clientElement) or "N/A" -- client serial, or "N/A" if not possible, for some reason
local logSource = inspect(sourceElement) -- in-depth view of source element
local logText = -- fill our report with data
"*\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) -- print it to debug
if (not punishPlayerOnDetect or skipPunishment) then -- we don't want to punish player for some reason
return true -- stop here
end
if (punishmentBan) then -- if it's ban
banPlayer(clientElement, banByIP, banByUsername, banBySerial, punishedBy, punishmentReason, banTime) -- remove his presence from server
else -- otherwise
kickPlayer(clientElement, punishedBy, punishmentReason) -- simply kick player out of server
end
return true -- all done, report success
end
function onServerEvent(clientData)
--[[
Assume this server event (function) is called in such way:
local dataToPass = 10
triggerServerEvent("onServerEvent", localPlayer, dataToPass)
]]
local shouldProcessServerCode = processServerEventData(
client, -- client element - responsible for calling event
source, -- source element - passed in triggerServerEvent (as 2nd argument)
eventName, -- name of event - in this case 'onServerEvent'
{
checkEventData = { -- we want to verify everything what comes from client
{
eventData = source, -- first to check, source variable
equalTo = client, -- we want to check whether it matches player who called event
debugData = "source", -- helper details which would be shown in report
},
{
eventData = clientData, -- let's check the data which client sent to us
allowedDataTypes = {
["number"] = true, -- we want it to be only number
},
allowedNumberRange = {1, 100}, -- in range of 1 to 100
debugData = "clientData", -- if something goes wrong, let server know where (it will appear in debug report)
},
},
}
)
if (not shouldProcessServerCode) then -- something isn't right, no green light for processing code behind this scope
return false -- stop code execution
end
-- do code as usual
end
addEvent("onServerEvent", true)
addEventHandler("onServerEvent", root, onServerEvent)
function onServerAdminEvent(playerToBan)
--[[
Assume this server admin event (function) is called in such way:
local playerToBan = getPlayerFromName("playerToBan")
triggerServerEvent("onServerAdminEvent", localPlayer, playerToBan)
]]
local shouldProcessServerCode = processServerEventData(
client, -- client element - responsible for calling event
source, -- source element - passed in triggerServerEvent (as 2nd argument)
eventName, -- name of event - in this case 'onServerAdminEvent'
{
checkACLGroup = { -- we need to check whether player who called event belongs to ACL groups
"Admin", -- in this case admin group
},
checkEventData = { -- we want to verify everything what comes from client
{
eventData = source, -- first to check, source variable
equalTo = client, -- we want to check whether it matches player who called event
debugData = "source", -- helper details which would be shown in report
},
{
eventData = playerToBan, -- let's check the data which client sent to us
allowedDataTypes = {
["player"] = true, -- we want it to be player
},
debugData = "playerToBan", -- if something goes wrong, let server know where (it will appear in debug report)
},
},
}
)
if (not shouldProcessServerCode) then -- something isn't right, no green light for processing code behind this scope
return false -- stop code execution
end
-- do code as usual
end
addEvent("onServerAdminEvent", true)
addEventHandler("onServerAdminEvent", root, onServerAdminEvent)
Securing server-only events
- It is very important to disable remote triggering ability in addEvent, to prevent calling server-side only events from client-side.
- Admin styled events should be verifying player ACL rights, either by isObjectInACLGroup or hasObjectPermissionTo.
This example shows how you should make event server-side only.
function onServerSideOnlyEvent()
-- do some server-side stuff
end
addEvent("onServerSideOnlyEvent", false) -- set second argument (allowRemoteTriger) to false, so it can't be called from client
addEventHandler("onServerSideOnlyEvent", root, onServerSideOnlyEvent) -- associate our event with function onServerSideOnlyEvent
Securing projectiles & explosions
This section (and code is work in progress) - use at your own risk.
Handling non-registered events
See: onPlayerTriggerInvalidEvent