Script security

From Multi Theft Auto: Wiki
Jump to navigation Jump to search

Awareness of client memory

Starting from very basics:

  • You should be aware that everything you store on client-side is at risk, this includes Lua itself. Any confidential (and/or) important data could be accessed by malicious clients.
  • To keep sensitive data (and/or) Lua logic safe - use server-side.
  • Do note that scripts marked as shared act also as client code, which means that everything above applies to them. For example defining:
<script src="script.lua" type="shared"/> <!-- this script will run separately both on client and server -->

Is same as doing:

<script src="script.lua" type="client"/> <!-- define it separately on client -->
<script src="script.lua" type="server"/> <!-- do the same, but on server -->

Additional protection layer

In order to make things slightly harder* for those having bad intentions towards your server, you can make use of cache attribute (and/or Lua compile (also known as Luac) with extra obfuscation set to level 3 - API) available in meta.xml, along with configuring MTA's built-in AC by toggling SD (Special detections), see: Anti-cheat guide.

<script src="shared.lua" type="shared" cache="false"/> <!-- cache="false" indicates that this Lua file won't be saved on player's PC -->
<script src="client.lua" type="client" cache="false"/> <!-- cache="false" indicates that this Lua file won't be saved on player's PC -->
  • Slightly harder but not impossible for some to obtain client code, yet does good job at keeping most people away from inspecting your .lua files - those looking for possible logic flaws (bugs) or missing/incorrect security based checks.
  • Can be used on both client and shared script type (has no effect on server-sided Lua).
  • It doesn't remove Lua files which were previously downloaded.

Detecting and dealing with the effects of rogue clients

To ensure minimum damage when a player with a hacked client connects to your server:

  • When making scripts, remember to never trust data coming from a client.
  • When reviewing scripts for possible security holes. Look at any data coming from the client that is being trusted.

A hacked client could send anything, so all client data received by server scripts should be validated before use. Mostly, it will be done either by setElementData or triggerServerEvent.

Validating client setElementData

  • All parameters including source can be faked and should not be trusted.
  • Global variable client can be trusted.

Click to collapse [-]
Server

Example of basic element data anti-cheat.

local function reportAndRevertDataChange(clientElement, sourceElement, dataKey, oldValue, newValue) -- helper function to log and revert changes
	local logClient = getPlayerName(clientElement)
	local logSource = tostring(sourceElement)
	local logOldValue = tostring(oldValue)
	local logNewValue = tostring(newValue)
	local logText = -- fill our report with data
		"=======================================\n"..
		"Possible rogue client!\n"..
		"clientElement: "..logClient.."\n"..
		"sourceElement: "..logSource.."\n"..
		"dataKey: "..dataKey.."\n"..
		"oldValue: "..logOldValue.."\n"..
		"newValue: "..logNewValue.."\n"..
		"======================================="

	outputConsole(logText, root) -- print it to console
	setElementData(sourceElement, dataKey, oldValue) -- revert changes, it will call onElementDataChange event, but will fail (stop) on first condition - because server (not client) forced change
end

function onElementDataChangeBasicAC(dataKey, oldValue, newValue)
	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)

Validating client triggerServerEvent

  • All parameters including source can be faked and should not be trusted.
  • Global variable client can be trusted.

Click to collapse [-]
Server

Example validation of event parameters

addEvent("onRaiseTheRoof", true)
addEventHandler("onRaiseTheRoof", resourceRoot,
    function(arg1, arg2)
        -- Check data is coming from a client
        if client then

            -- The source for this event is always 'resourceRoot'
            if source ~= resourceRoot then
                reportNaughtyness( eventName, client, "source" )
                return
            end

            -- arg1 should be the player
            if arg1 ~= client then
                reportNaughtyness( eventName, client, "arg1" )
                return
            end

            -- arg2 should be between 1 and 100
            if arg2 < 1 or arg2 >100 then
                reportNaughtyness( eventName, client, "arg2" )
                return
            end
        end

        --
        -- Do usual code for 'onRaiseTheRoof'
        --
    end
)

-- Helper function to log illegal things
function reportNaughtyness( eventName, client, reason )
    -- Report
    outputConsole( "Possible rogue client!"
            .. " client:" .. tostring(getPlayerName(client))
            .. " eventName:" .. tostring(eventName)
            .. " reason:" .. tostring(reason)
            )
end



Click to collapse [-]
Server

Example validation of event parameters for admin style event

addEvent("onRequestBanPlayer", true)
addEventHandler("onRequestBanPlayer", resourceRoot,
    function(arg1, arg2)
        -- Check data is coming from a client
        if client then

            -- Check client has permission to do the deed
            if not canPlayerBan(client) then
                reportNaughtyness( eventName, client, "client" )
                return
            end

        end

        --
        -- Do usual code for 'onRequestBanPlayer'
        --
    end
)


Validating client ACL rights

In server side event handlers, always check that client global variable (which refers to player which truthfully called event) has permission before doing anything. This will stop hackers and script bugs from destroying your server.

Firstly, and example of BAD, INSECURE script:

Click to collapse [-]
Server

BAD, INSECURE script:

-- NO PROBLEM HERE: Command 'showgui' will only show the gui for admins
function showGui(player)
    if isObjectInGroup( player, "admin" ) then
        -- Only trigger client GUI for admins
        triggerEvent( player, "onShowGui", resourceRoot )
    end
end
addCommandHandler("showgui", showGui)

-- MISTAKE HERE: Incorrectly assume onMyGuiCommand can only be called by admins
-- Script bugs and hackers mean this function can be called by anyone
function onMyGuiCommand(name)
    aclGroupAddObject(aclGetGroup("admin"), "user."..name)
    outputServerLog( "DO NOT USE THIS IS WRONG " )
end
addEventHandler("onMyGuiCommand", resourceRoot, onMyGuiCommand)

onMyGuiCommand does not check if the sending player has permission, and is depending on code having no bugs, and no hackers in the world. Simple to fix by adding a client global variable check at the start. (Note: checking source will not fix the problem - It has to be client)

Click to collapse [-]
Server

Fixed onMyGuiCommand:

function onMyGuiCommand(name)
    -- Check 'client' really does have permission
    local clientAccountName = getAccountName( getPlayerAccount( client ) )
    if not isObjectInACLGroup ( "user." .. clientAccountName, aclGetGroup ( "Admin" ) ) ) then
        outputServerLog( "ACCESS VIOLATION by " .. getPlayerName( client ) )
        return
    end
    -- Client has permission now
    aclGroupAddObject(aclGetGroup("admin"), "user."..name)
end
addEventHandler("onMyGuiCommand", resourceRoot, onMyGuiCommand)