Explorer
KNOW-PAT-082

FiveM - NUI Callbacks securises : Protection contre injection et falsification

Domaine
framework
Type
pattern
Priorité
P1

Parent : [[INDEX-FIVEM-FRAMEWORK]]

FiveM - NUI Callbacks securises : Protection contre injection et falsification

Probleme

Les NUI (Native UI) de FiveM permettent d'afficher des interfaces HTML/JS dans le jeu. Les callbacks NUI (RegisterNUICallback) communiquent du frontend vers le backend Lua client. Si ces callbacks declenchent des TriggerServerEvent sans validation, un joueur peut manipuler le NUI via la console du navigateur ou injecter du JS pour falsifier les appels.

Contexte

  • NUI = Overlay HTML/JS/CSS dans FiveM (Cef en interne)
  • SendNUIMessage : Lua → JS
  • RegisterNUICallback : JS → Lua
  • Le JS cote NUI est controle par le client (modifiable via console DevTools)
  • Un callback NUI mal securise = vector d'attaque pour trigger des events serveur

Solution

1. RegisterNUICallback avec validation contextuelle

-- Le callback NUI ne fait que collecter, le serveur valide
RegisterNUICallback('requestDocumentSave', function(data, cb)
    local src = PlayerId()
    
    -- Validation 1 : Le joueur est-il bien en train d'utiliser l'interface ?
    if not isPlayerUsingDocumentUI(src) then
        cb({ success = false, error = 'UI not active' })
        return
    end
    
    -- Validation 2 : Les donnees sont-elles dans le format attendu ?
    if type(data.documentType) ~= 'string' or #data.documentType > 50 then
        cb({ success = false, error = 'Invalid document type' })
        return
    end
    
    -- Validation 3 : Le document type est-il valide ?
    local validTypes = { 'id_card', 'license', 'report', 'mugshot' }
    if not hasValue(validTypes, data.documentType) then
        cb({ success = false, error = 'Unknown document type' })
        return
    end
    
    -- On envoie au serveur avec un TOKEN de session NUI
    local sessionToken = generateNUISessionToken(src)
    TriggerServerEvent('documents:saveValidated', {
        documentType = data.documentType,
        fields = sanitizeFields(data.fields),
        sessionToken = sessionToken,
        timestamp = GetGameTimer()
    })
    
    cb({ success = true, token = sessionToken })
end)

local nuiSessions = {}
function generateNUISessionToken(playerId)
    local token = string.format('%d_%d_%s', playerId, GetGameTimer(), math.random(100000, 999999))
    nuiSessions[playerId] = { token = token, created = GetGameTimer(), expires = GetGameTimer() + 30000 }
    return token
end

2. Server-side : Validation du token NUI

local activeNUISessions = {}

function validateNUISession(src, token)
    local session = activeNUISessions[src]
    if not session then return false end
    if session.token ~= token then
        activeNUISessions[src] = nil
        return false
    end
    if GetGameTimer() > session.expires then
        activeNUISessions[src] = nil
        return false
    end
    activeNUISessions[src] = nil
    return true
end

RegisterNetEvent('documents:saveValidated')
AddEventHandler('documents:saveValidated', function(data)
    local src = source
    if not validateNUISession(src, data.sessionToken) then
        logSuspiciousActivity(src, 'Invalid NUI token for document save')
        return
    end
    -- Validation metier...
    saveDocument(src, data.documentType, data.fields)
end)

RegisterNetEvent('documents:openUI')
AddEventHandler('documents:openUI', function()
    local src = source
    local token = ('session_%d_%d_%d'):format(src, os.time(), math.random(1000000, 9999999))
    activeNUISessions[src] = { token = token, created = GetGameTimer(), expires = GetGameTimer() + 300000, uiType = 'documents' }
    TriggerClientEvent('documents:sessionCreated', src, token)
end)

3. Protection contre les devtools / injection JS

<script>
(function() {
    'use strict';
    
    const threshold = 160;
    let devToolsOpen = false;
    
    const checkDevTools = () => {
        const widthThreshold = window.outerWidth - window.innerWidth > threshold;
        const heightThreshold = window.outerHeight - window.innerHeight > threshold;
        
        if (widthThreshold || heightThreshold) {
            if (!devToolsOpen) {
                devToolsOpen = true;
                fetch('https://' + GetParentResourceName() + '/devToolsDetected', {
                    method: 'POST',
                    body: JSON.stringify({ detected: true })
                });
            }
        }
    };
    
    setInterval(checkDevTools, 1000);
})();
</script>

4. Rate limiting sur les callbacks NUI

local nuiCallbackLimits = {}

function rateLimitNUICallback(callbackName, maxCallsPerSecond)
    local now = GetGameTimer()
    if not nuiCallbackLimits[callbackName] then
        nuiCallbackLimits[callbackName] = { count = 0, windowStart = now }
    end
    local limit = nuiCallbackLimits[callbackName]
    if now - limit.windowStart > 1000 then
        limit.count = 0
        limit.windowStart = now
    end
    limit.count = limit.count + 1
    if limit.count > maxCallsPerSecond then
        return false
    end
    return true
end

5. Isolation du NUI : pas de donnees sensibles dans le DOM

<script>
window.addEventListener('message', (event) => {
    const data = event.data;
    if (data.type === 'showDocument') {
        document.getElementById('doc-content').textContent = data.content;
    }
    // PAS de donnees de permission ici
});

function onSaveButtonClick() {
    const content = document.getElementById('doc-content').textContent;
    fetch('https://' + GetParentResourceName() + '/requestSave', {
        method: 'POST',
        body: JSON.stringify({ content: content })
    });
}
</script>

Checklist de validation

  • Les callbacks NUI ne declenchent JAMAIS de TriggerServerEvent sans validation
  • Token de session NUI genere cote serveur et valide server-side
  • Rate limiting sur tous les callbacks NUI modifiant l'etat
  • Le NUI est "read-only" pour les permissions (pas de donnees sensibles dans le DOM)
  • Detection DevTools cote client (layer de defense)
  • Sanitization des donnees recues du NUI avant traitement
  • Les callbacks NUI sont minimaux : collecte → validation → server event
  • Pas de logique metier dans le JS du NUI (tout cote serveur)
  • Obfuscation/minification du JS du NUI
  • Sessions NUI avec expiration (TTL court)
  • Log des tentatives de callback NUI avec token invalide

Anti-pattern associe

  • [[KNOW-ANT-FIVEM-002-client-side-validation-only|KNOW-ANT-FIVEM-002]] — Client-side validation only

References

Liens connexes

  • [[KNOW-ANT-FIVEM-002-client-side-validation-only|KNOW-ANT-FIVEM-002]]