RU/Script security

From Multi Theft Auto: Wiki
Revision as of 19:13, 8 January 2026 by Flox (talk | contribs) (Created page with "==Внимание к клиентской памяти == Начнем с самых основ: * Вы должны знать, что все, что вы храните на стороне клиента, подвержено риску, в том числе и lua-файлы. Злоумышленники могут получить доступ к любым конфиденциальным (и/или) важным данным, которые хранятся ил...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Внимание к клиентской памяти

Начнем с самых основ:

  • Вы должны знать, что все, что вы храните на стороне клиента, подвержено риску, в том числе и 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, как по соображениям безопасности, так и по соображениям производительности, описанным в разделе "события".

Template:Важное примечание

  • Все параметры, включая source, могут быть подделаны, и им не следует доверять.
  • Глобальной переменной client можно доверять.


Click to collapse [-]
Server

Пример защиты данных базового элемента от обмана.

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)
Click to collapse [-]
Server

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

Click to collapse [-]
Server

Пример функции защиты от мошенничества, разработанной для событий, используемых для проверки данных как для обычных, так и для административных событий, которые вызываются со стороны клиента.

--[[
	Для максимальной безопасности установите 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.

Click to collapse [-]
Server

В этом примере показано, как вы должны создавать события только на стороне сервера.

function onServerSideOnlyEvent()
	-- делаем какие-то вещи на стороне сервера
end
addEvent("onServerSideOnlyEvent", false) -- установите второй аргумент (allowRemoteTriger) в false, чтобы его нельзя было вызвать с клиента
addEventHandler("onServerSideOnlyEvent", root, onServerSideOnlyEvent) -- связываем наше событие с функцией onServerSideOnlyEvent

Защита снарядов и взрывных устройств

Этот раздел (и код - работа продолжается) - используйте на свой страх и риск.


Click to expand [+]
Server
Click to expand [+]
Server
Click to expand [+]
Server

Обработка незарегистрированных событий

Смотрите: onPlayerTriggerInvalidEvent

Обработка нежелательных событий

Смотрите: onPlayerTriggerEventThreshold