Script security
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 available in meta.xml
<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.
How to never trust data coming from a client
A hacked client could send anything, so all client data received by server scripts should be validated before use. Mostly, it will 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.
Example validation of element data changes
addEventHandler("onElementDataChange", root, function(dataName, oldValue) -- Check data is coming from a client if client then -- Validate 'special_thing' if dataName == "special_thing" then -- 'special_thing' can only be set by a client on its own player if client ~= source then -- Illegal activity here, so log and revert the change reportAndRevertDataChange( dataName, oldValue, source, client ) end end -- Validate 'flag_waving' if dataName == "flag_waving" then -- 'flag_waving' can only be set by a client on its own vehicle local vehicle = getPedOccupiedVehicle(client) if vehicle ~= source then -- Illegal activity here, so log and revert the change reportAndRevertDataChange( dataName, oldValue, source, client ) end end end end ) -- Helper function to log and revert changes function reportAndRevertDataChange( dataName, oldValue, source, client ) -- Report outputConsole( "Possible rogue client!" .. " client:" .. tostring(getPlayerName(client)) .. " dataName:" .. tostring(dataName) .. " oldValue:" .. tostring(oldValue) .. " newValue:" .. tostring(getElementData(source,dataName)) .. " source:" .. tostring(source) ) -- Revert (Note this will cause an onElementDataChange event, but 'client' will be nil) setElementData( source, dataName, oldValue ) end
Validating client triggerServerEvent
- All parameters including source can be faked and should not be trusted.
- Global variable client can be trusted.
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
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:
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)
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)