[Experiment] NUI Callback Strict Mode (requires opt-in)

We’re happy to announce an improvement to NUI security called NUI Callback Strict Mode :construction:

This experiment is available to all FiveM players in the Canary channel running ver. 9549 or newer. If you are a resource developer and would like to learn more & help us test how the changes impact player experience, please read the following thoroughly.

:question: Why are we making this change?

At the moment, most NUI callbacks can be accessed by any resource. In case a callback provides sensitive data or management functionality, it can potentially be abused by a malicious resource. NUI Callback Strict Mode introduces a new resource manifest flag that ensures foreign resources won’t be able to tap into your callbacks.

:thinking: Considerations before testing / opting in

Resources that use callbacks for cross-resource communication need special care. If this doesn’t apply to you, you can safely continue to the next section.
There are 2 paths you can take if your resource falls into this category:

  • Switch to JavaScript exports, these provide you with a way to synchronously exchange and process data between resources on script level.
  • For least friction, you can switch from callbacks to events. This is preferred if you share simple non-sensitive data between resources as events are broadcast globally.

In all cases you’ll have to register a new / equivalent NUI Callback on the side of the receiving resource to bubble the data retrieved through any of the above means from scripts to NUI.

Both conversion examples can be found in the following details section:

💱 NUI Callback alternatives for cross-resource communication

The examples found here feature resource ncb_sample_provider which provides a getItemId data processing function, and resource ncb_sample_consumer that contacts the provider to do said data processing.

With Strict Mode on, attempting to fetch https://ncb_sample_provider/getItemId will no longer work and instead yield a warning in the console. (call to 'ncb_sample_provider/getItemId' from 'ncb_sample_consumer' has been blocked by NUI Callback Strict Mode)

:one: Alternative #1: Switching to JavaScript exports

Going this route means you’ll have to switch to implementing your data processing functions in JavaScript. If your resource doesn’t use JS at the moment, make sure to create a new file with the .js extension and add it to the list of script files in your resource’s manifest. (fxmanifest.lua)

Step 1: Converting the Provider’s NUI callbacks to JS exports

Let’s take a simple NUI callback which replies with the itemId property of the data parameter:

RegisterNUICallback('getItemId', function(data, cb)
    -- retrieve property
    local resultData = data.itemId
    -- reply with retrieved data
    cb(resultData)
end)

Here’s how it would look as a JavaScript export:

exports("getItemId", (data) => {
  resultData = data.itemId;
  return resultData;
});
Step 2: Adding the proxy NUI callback to the Consumer resource

A new NUI callback will have to be registered within the Consumer resource. Its purpose is to talk to the Provider using the events we defined in Step 1. For clarity, we recommend taking the name of the original callback and suffixing it with Proxy. An event handler which catches the appropriate tagged response is added as well. This also performs the final step and returns data to the pending getItemIdProxy request.

RegisterNUICallback('getItemIdProxy', function(data, cb)
    -- call the JS export
    local resultData = exports.ncb_sample_provider:getItemId(data)
    -- reply with retrieved data
    cb(resultData)
end)

:two: Alternative #2: Creating proxy callbacks and using events to push data

:warning: Keep in mind that while this alternative is the easiest to adopt, using events to push data means it is broadcast globally and resources outside of your scope may choose to process it.

Step 1: Converting the Provider’s NUI callbacks to event handlers

Let’s take a simple NUI callback which replies with the itemId property of the data parameter:

RegisterNUICallback('getItemId', function(data, cb)
    -- retrieve property
    local resultData = data.itemId
    -- reply with retrieved data
    cb(resultData)
end)

Here’s how it would look as an event handler:

AddEventHandler("ncb_sample_provider:getItemId", function(__tag, data)
    -- retrieve property
    local resultData = data.itemId
    -- reply with retrieved data
    TriggerEvent("ncb_sample_provider:getItemIdCallback", __tag, resultData)
end)

We recommend prefixing the name of the function with the name of the resource when converting to events because event names exist on a global level.

The callback event triggered at the end of the handler is what the Consumer will have to subscribe to, we recommend taking the name of the data processing event and simply suffixing with Callback.

Step 2: Adding a NUI callbacks and event handlers to the Consumer resource

A new NUI callback will have to be registered within the Consumer resource. Its purpose is to talk to the Provider using the events we defined in Step 1. For clarity, we recommend taking the name of the original callback and suffixing it with Proxy. An event handler which catches the appropriate tagged response is added as well. This also performs the final step and returns data to the pending getItemIdProxy request.

local cbTable = {}
RegisterNUICallback('getItemIdProxy', function(data, cb)
    -- generate unique tag for processing this specific request
    math.randomseed(GetGameTimer())
	local __tag = math.random()
	-- save request completion callback
	cbTable[__tag] = cb
	-- fire a request to the Provider
    TriggerEvent("ncb_sample_provider:getItemId", __tag, data)
end)
AddEventHandler("ncb_sample_provider:getItemIdCallback", function(__tag, itemIdData)
    local cb = cbTable[__tag]
	cbTable[__tag] = nil
	cb(itemIdData)
end)

:checkered_flag: The final step

Based on the example resource names and callback names provided above, you’ll have to change the request’s domain from nuicb_sample_provider to nuicb_sample_consumer (the resource within which the JS you’re editing resides) and add the word Proxy at the end of the URI.

E.g. https://ncb_sample_provider/getItemId :arrow_right: https://ncb_sample_consumer/getItemIdProxy.

The examples above ensure a synchronous flow, requiring no extra changes to the JS that handles data returned by the callback.

:control_knobs: How to opt-in?

  1. Make sure you’re running FiveM ver. 9549 or newer
  2. Add the following line into the manifest file of all resources you wish to test this experiment with:
nui_callback_strict_mode 'true'
  1. Make sure to save your changes and reload the resources / restart the server as necessary
  2. Launch into the game
  3. Test the functionality of all resource(s) enrolled into the experiment to the fullest extent and let us know about any issues you encountered. (Suggestions are welcome as well, but issues/bugs will naturally be given a higher priority as the experiment gets refined over time.)

Every time NUI Callback Strict Mode blocks a request from being routed, an error will be printed to the game’s console.

:hourglass_flowing_sand: Will Strict Mode be the new default in the future?

Yes. Once we smooth out the edges, the experiment will change from being opt-in to being opt-out. After developers have been given enough time to migrate, the previous insecure behavior may be removed completely, although this final step is still subject to change.

Opting out will be as simple as adding this line to your resource manifest:

nui_callback_strict_mode 'false'

Thank you for your interest and your time devoted to helping us test experiments, we greatly appreciate it!

20 Likes