Skip to content

Commit

Permalink
feat(resource): check origin for all nui callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
tabarra committed Jan 13, 2024
1 parent 49fa854 commit 52a857a
Show file tree
Hide file tree
Showing 12 changed files with 99 additions and 39 deletions.
11 changes: 2 additions & 9 deletions docs/dev_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,7 @@
- [x] global hotkey to go to the player filter
- careful to also handle child iframe and terminal canvas
- add util function that gets a KeyboardEvent and returns the name of the hotkey pressed, then apply it to child iframe, global keydown handler, and xterm
- [ ] FIXME: check if we need or can do something to prevent NUI CSRF



=======================================================================




- [x] FIXME: check if we need or can do something to prevent NUI CSRF



Expand Down Expand Up @@ -63,6 +55,7 @@
# TODO: v7.1+
- [ ] Remove old live console legacy code
- [ ] fix the tsc build
- [ ] can I remove `/nui/resetSession`? I think we don't even use cookies anymore

- [ ] NEW PAGE: Dashboard
- [ ] number callouts from legacy players page
Expand Down
58 changes: 58 additions & 0 deletions resource/cl_main.lua
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,61 @@ CreateThread(function()
TriggerEvent('chat:removeSuggestion', suggestion)
end
end)


-- =============================================
-- Helper to protect the NUI callbacks from CSRF attacks
-- NOTE: This is a temporary fix for the NUI callback Origin issue
-- =============================================
--- Check if a NUI callback is from the correct Origin
--- technically no request should come from nui://monitor, since the manifest version is cerulean
---@param headers table
---@return boolean
function IsNuiRequestOriginValid(headers)
if type(headers) ~= 'table' then
return false --no clue
end
if headers['Origin'] == nil then
return true --probably legacy page
end
if type(headers['Origin']) ~= 'string' or headers['Origin'] == '' then
return false --no clue
end

if headers['Origin'] == 'https://cfx-nui-monitor' then
return true --probably self
end
if headers['Origin'] == 'https://monitor' then
return true --probably legacy iframe inside web iframe
end

-- warn admin of possible csrf attempt
if menuIsAccessible and sendPersistentAlert then
local msg = ('ATTENTION! txAdmin received a NUI message from the origin "%s" which is not approved. This likely means that that resource is vulnerable to XSS which has been exploited to inject txAdmin commands. It is recommended that you fix the vulnerability or remove that resource completely. For more information: discord.gg/txAdmin.') :format(headers['Origin'])
sendPersistentAlert('csrfWarning', 'error', msg, false)
end

return false
end

--- Wrapper for RegisterRawNuiCallback which mimics the behavior of RegisterNUICallback
--- but checks the origin of the request to prevent CSRF attacks
function RegisterSecureNuiCallback(callbackName, funcCallback)
RegisterRawNuiCallback(callbackName, function(req, nuiCallback)
if not IsNuiRequestOriginValid(req.headers) then
debugPrint(("^1Invalid NUI callback origin for %s"):format(callbackName))
return nuiCallback({
status = 403,
body = '{}',
})
end

-- calls the function
funcCallback(json.decode(req.body), function(data)
nuiCallback({
status = 200,
body = type(data) == 'table' and json.encode(data) or '{}',
})
end)
end)
end
2 changes: 1 addition & 1 deletion resource/cl_playerlist.lua
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ end)


-- Triggered when the "player" tab opens in the menu, and every 5s after that
RegisterNUICallback('signalPlayersPageOpen', function(_, cb)
RegisterSecureNuiCallback('signalPlayersPageOpen', function(_, cb)
TriggerServerEvent('txsv:req:plist:getDetailed', requirePlayerNames)
requirePlayerNames = false
cb({})
Expand Down
8 changes: 4 additions & 4 deletions resource/menu/client/cl_base.lua
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ end)

--[[ NUI Callbacks ]]
-- Triggered whenever we require full focus, cursor and keyboard
RegisterNUICallback('focusInputs', function(shouldFocus, cb)
RegisterSecureNuiCallback('focusInputs', function(shouldFocus, cb)
debugPrint('NUI Focus + Keep Input ' .. tostring(shouldFocus))
-- Will prevent mouse focus on initial menu mount as the useEffect emits there
if not isMenuVisible then
Expand All @@ -154,7 +154,7 @@ RegisterNUICallback('focusInputs', function(shouldFocus, cb)
end)


RegisterNUICallback('reactLoaded', function(_, cb)
RegisterSecureNuiCallback('reactLoaded', function(_, cb)
debugPrint("React loaded, requesting ServerCtx.")

CreateThread(function()
Expand All @@ -171,7 +171,7 @@ RegisterNUICallback('reactLoaded', function(_, cb)
end)

-- When the escape key is pressed in menu
RegisterNUICallback('closeMenu', function(_, cb)
RegisterSecureNuiCallback('closeMenu', function(_, cb)
isMenuVisible = false
debugPrint('Releasing all NUI Focus')
SetNuiFocus(false)
Expand All @@ -181,7 +181,7 @@ end)


-- Audio play callback
RegisterNUICallback('playSound', function(sound, cb)
RegisterSecureNuiCallback('playSound', function(sound, cb)
playLibrarySound(sound)
cb({})
end)
Expand Down
2 changes: 1 addition & 1 deletion resource/menu/client/cl_freeze.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ local function sendFreezeAlert(isFrozen)
end
end

RegisterNUICallback('togglePlayerFreeze', function(data, cb)
RegisterSecureNuiCallback('togglePlayerFreeze', function(data, cb)
local targetPlayerId = tonumber(data.id)
if targetPlayerId == GetPlayerServerId(PlayerId()) then
return sendSnackbarMessage('error', 'nui_menu.player_modal.actions.interaction.notifications.freeze_yourself', true)
Expand Down
25 changes: 12 additions & 13 deletions resource/menu/client/cl_main_page.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@ if not TX_MENU_ENABLED then return end
--[[ NUI CALLBACKS ]]

-- Data is a object with x, y, z
RegisterNUICallback('tpToCoords', function(data, cb)
RegisterSecureNuiCallback('tpToCoords', function(data, cb)
debugPrint(json.encode(data))
TriggerServerEvent('txsv:req:tpToCoords', data.x + 0.0, data.y + 0.0, data.z + 0.0)
cb({})
end)

RegisterNUICallback('tpToWaypoint', function(_, cb)
RegisterSecureNuiCallback('tpToWaypoint', function(_, cb)
TriggerServerEvent('txsv:req:tpToWaypoint')
cb({})
end)

RegisterNUICallback('tpToPlayer', function(data, cb)
RegisterSecureNuiCallback('tpToPlayer', function(data, cb)
TriggerServerEvent('txsv:req:tpToPlayer', tonumber(data.id))
cb({})
end)

RegisterNUICallback('tpBack', function(_, cb)
RegisterSecureNuiCallback('tpBack', function(_, cb)
if lastTpCoords then
TriggerServerEvent('txsv:req:tpToCoords', lastTpCoords.x, lastTpCoords.y, lastTpCoords.z)
cb({})
Expand All @@ -34,12 +34,12 @@ RegisterNUICallback('tpBack', function(_, cb)
end
end)

RegisterNUICallback('summonPlayer', function(data, cb)
RegisterSecureNuiCallback('summonPlayer', function(data, cb)
TriggerServerEvent('txsv:req:bringPlayer', tonumber(data.id))
cb({})
end)

RegisterNUICallback('copyCurrentCoords', function(_, cb)
RegisterSecureNuiCallback('copyCurrentCoords', function(_, cb)
local ped = PlayerPedId()
local curCoords = GetEntityCoords(ped)
local currHeading = GetEntityHeading(ped)
Expand All @@ -48,37 +48,36 @@ RegisterNUICallback('copyCurrentCoords', function(_, cb)
cb({ coords = stringCoords })
end)

RegisterNUICallback('clearArea', function(radius, cb)
RegisterSecureNuiCallback('clearArea', function(radius, cb)
TriggerServerEvent('txsv:req:clearArea', radius)
cb({})
end)

-- [[ Spawn weapon (only in dev, for now) ]]
RegisterNUICallback('spawnWeapon', function(weapon, cb)
RegisterSecureNuiCallback('spawnWeapon', function(weapon, cb)
if not TX_DEBUG_MODE then return end
debugPrint("Spawning weapon: " .. weapon)
GiveWeaponToPed(PlayerPedId(), weapon, 500, false, true)
cb({})
end)

RegisterNUICallback('healPlayer', function(data, cb)
RegisterSecureNuiCallback('healPlayer', function(data, cb)
TriggerServerEvent('txsv:req:healPlayer', tonumber(data.id))
cb({})
end)

RegisterNUICallback('healMyself', function(_, cb)
RegisterSecureNuiCallback('healMyself', function(_, cb)
TriggerServerEvent('txsv:req:healMyself')
cb({})
end)

RegisterNUICallback('healAllPlayers', function(_, cb)
RegisterSecureNuiCallback('healAllPlayers', function(_, cb)
TriggerServerEvent('txsv:req:healEveryone')
cb({})
end)

-- Data will be an object with a message attribute
RegisterNUICallback('sendAnnouncement', function(data, cb)
debugPrint(data.message)
RegisterSecureNuiCallback('sendAnnouncement', function(data, cb)
TriggerServerEvent('txsv:req:sendAnnouncement', data.message)
cb({})
end)
Expand Down
2 changes: 1 addition & 1 deletion resource/menu/client/cl_player_ids.lua
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ local function togglePlayerIDsHandler()
TriggerServerEvent('txsv:req:showPlayerIDs', not isPlayerIdsEnabled)
end

RegisterNUICallback('togglePlayerIDs', function(_, cb)
RegisterSecureNuiCallback('togglePlayerIDs', function(_, cb)
togglePlayerIDsHandler()
cb({})
end)
Expand Down
2 changes: 1 addition & 1 deletion resource/menu/client/cl_player_mode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ RegisterCommand('txAdmin:menu:noClipToggle', function()
end)

-- Menu callback to change the player mode
RegisterNUICallback('playerModeChanged', function(mode, cb)
RegisterSecureNuiCallback('playerModeChanged', function(mode, cb)
askChangePlayerMode(mode)
cb({})
end)
Expand Down
2 changes: 1 addition & 1 deletion resource/menu/client/cl_spectate.lua
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ end


-- Register NUI callback
RegisterNUICallback('spectatePlayer', function(data, cb)
RegisterSecureNuiCallback('spectatePlayer', function(data, cb)
TriggerServerEvent('txsv:req:spectate:start', tonumber(data.id))
cb({})
end)
Expand Down
6 changes: 3 additions & 3 deletions resource/menu/client/cl_trollactions.lua
Original file line number Diff line number Diff line change
Expand Up @@ -150,17 +150,17 @@ end)


--[[ NUI Callbacks ]]
RegisterNUICallback('drunkEffectPlayer', function(data, cb)
RegisterSecureNuiCallback('drunkEffectPlayer', function(data, cb)
TriggerServerEvent('txsv:req:troll:setDrunk', tonumber(data.id))
cb({})
end)

RegisterNUICallback('setOnFire', function(data, cb)
RegisterSecureNuiCallback('setOnFire', function(data, cb)
TriggerServerEvent('txsv:req:troll:setOnFire', tonumber(data.id))
cb({})
end)

RegisterNUICallback('wildAttack', function(data, cb)
RegisterSecureNuiCallback('wildAttack', function(data, cb)
TriggerServerEvent('txsv:req:troll:wildAttack', tonumber(data.id))
cb({})
end)
8 changes: 4 additions & 4 deletions resource/menu/client/cl_vehicle.lua
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ end

local gameSpawnReqHandler = IS_FIVEM and handleSpawnRequestFivem or handleSpawnRequestRedm

RegisterNUICallback('spawnVehicle', function(data, cb)
RegisterSecureNuiCallback('spawnVehicle', function(data, cb)
if type(data) ~= 'table' or type(data.model) ~= 'string' then
error("Invalid spawnVehicle NUI callback data")
end
Expand All @@ -105,7 +105,7 @@ RegisterNUICallback('spawnVehicle', function(data, cb)
cb(spawnReqDone and {} or { e = true })
end)

RegisterNUICallback("deleteVehicle", function(data, cb)
RegisterSecureNuiCallback("deleteVehicle", function(data, cb)
local ped = PlayerPedId()
local veh = GetVehiclePedIsIn(ped, false)
if IS_REDM and IsPedOnMount(ped) then
Expand All @@ -121,7 +121,7 @@ RegisterNUICallback("deleteVehicle", function(data, cb)
end)


RegisterNUICallback('fixVehicle', function(_, cb)
RegisterSecureNuiCallback('fixVehicle', function(_, cb)
local ped = PlayerPedId()
local veh = GetVehiclePedIsIn(ped, false)
if (veh == 0) and not IsPedOnMount(ped) then
Expand All @@ -133,7 +133,7 @@ RegisterNUICallback('fixVehicle', function(_, cb)
end)


RegisterNUICallback('boostVehicle', function(_, cb)
RegisterSecureNuiCallback('boostVehicle', function(_, cb)
local ped = PlayerPedId()
local veh = GetVehiclePedIsIn(ped, false)
if IS_REDM and IsPedOnMount(ped) then
Expand Down
12 changes: 11 additions & 1 deletion resource/menu/client/cl_webpipe.lua
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,18 @@ RegisterRawNuiCallback('WebPipe', function(req, cb)
local headers = req.headers
local body = req.body
local method = req.method

debugPrint(("^3WebPipe[^1%d^3]^0 ^2%s ^4%s^0"):format(pipeCallbackCounter, method, path))

-- Check for CSRF attempt
if not IsNuiRequestOriginValid(headers) then
debugPrint(("^3WebPipe[^1%d^3]^0 ^1invalid origin^0"):format(pipeCallbackCounter))
return cb({
status = 403,
body = '{}',
})
end

-- Check if the request is cached
if staticCacheData[path] ~= nil then
debugPrint(("^3WebPipe[^1%d^3]^0 ^2answered from cache!"):format(pipeCallbackCounter))
local cacheEntry = staticCacheData[path]
Expand Down

0 comments on commit 52a857a

Please sign in to comment.