All advisories
HXA-2026-001CriticalOpen

Ungated native addon loading in the server JS runtime → native code execution inside FXServer

Product
FiveM / FXServer
Component
citizen-scripting-node (embedded libnode 22)
CVSS
9.0
Published
July 28, 2026

Summary

Server-side JavaScript resources on an FXServer run in an embedded Node.js (libnode 22). FiveM gates this runtime with a custom permission handler that intercepts access such as filesystem, child processes, workers and the inspector.

Native addon loading is not covered by that gate. Node 22 simply has no permission scope for addons — the corresponding mechanism (--allow-addons) only exists from Node 24 onwards. The permission callback is therefore never invoked when an addon is loaded. Any JavaScript resource can load a .node file (a compiled DLL) it ships in its own folder and thereby execute arbitrary native code directly inside the server process.

This is the maximal sandbox escape: it bypasses every script-level mitigation (VFS boundaries, filesystem permissions, Lua/JS sandboxing).

Root cause

The permission handler is the only gate for server JS. Its scope list (filesystem, child process, worker, inspector) matches the Node 22 upstream exactly — and Node 22 has no addon scope. libnode's binding layer therefore contains no permission check for the addon load path (dlopen). The installed handler is never consulted when a resource loads a native module.

There is no compensating control: no module whitelisting, and the gated filesystem layer is irrelevant because the operating system's native loader reads the DLL directly.

Impact

  • Full server compromise (code execution as the server process) from any installed third-party JS resource.
  • Persistence and stealth: native code can evade resource introspection and survive script-runtime restarts.
  • Availability: even a broken addon produces a persistent crash loop when combined with auto-restart.

Fix

  • Backport the Node 24 addon permission scope (kAddon, default-deny) into the patched libnode and enforce it in the dlopen path via the existing permission handler.
  • In the interim: block process.dlopen / require('*.node') in the runtime for non-system resources.
  • Long term: a module-type whitelist for third-party resources (JS/Lua only, no native modules).