Talos Vulnerability Report

TALOS-2026-2417

frangoteam FUXA /api/runscript authorization bypass vulnerability

October 29, 2026

CVE Number

CVE-2026-42949

Summary

A authorization bypass vulnerability exists in the /api/runscript functionality of FUXA (version(s): 1.3.2-2809). A specially crafted HTTP request can lead to execute priviledged operation. An attacker can make an unauthenticated HTTP request to trigger this vulnerability.

Confirmed Vulnerable Versions

The versions below were either tested or verified to be vulnerable by Talos or confirmed to be vulnerable by the vendor.

FUXA (version(s): 1.3.2-2809)

Product URLs

FUXA - https://github.com/frangoteam/FUXA

CVSSv3 Score

5.6 - CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L

CWE

CWE-285 - Improper Authorization

Details

FUXA is an open-source, web based HMI software developed by frangoteam.

Users of the FUXA software can build virtual dashboards of physical processes, enabling them to read and write live process data, execute automations at the click of a button, review historical process data, interact remotely with physical devices, etc.

Administrative users may develop ‘scripts’ which can, among other options, be bound to dashboard events, such as a particular widget being clicked, double clicked, moused-over, etc. These scripts can be configured to run locally in the browser or remotely on the FUXA host. When a user triggers one of these remote scripts to run, the browser will emit a POST request to the /api/runscript endpoint, which will execute the requested automation. This API endpoint has a flaw in how it validates whether the requesting user is authorized to execute the requested script, allowing an unauthenticated user to execute automations for which they are not permitted.

The flaw arises due to a mismatch in how the authorization functionality and the execution functionality reference the requested script. POST requests destined for /api/runscript are dispatched into server/api/scripts/index.js. We include the handler for these requests below.

/**
 * POST runscript
 * Run script, can be call with script id or script content as test
 */
scriptsApp.post("/api/runscript", secureFnc, function (req, res, next) {
  const permission = checkGroupsFnc(req);
  // [1] Recover the caller-supplied script object directly from the request
  const script = req.body?.params?.script;
  
  if (res.statusCode === 403) {
    runtime.logger.error("api post runscript: Tocken Expired");
  } else if (script?.test && (!req.isAuthenticated || !authJwt.haveAdminPermission(permission))) {
    res.status(401).json({ error: "unauthorized_error", message: "Unauthorized!" });
    runtime.logger.error("api post runscript: Unauthorized test mode");
    
  // [2] Authorize the script via `isAuthorised`
  } else if (!runtime.scriptsMgr.isAuthorised(script, permission)) {
    res.status(400).json({ error: "unauthorized_error", message: "Unauthorized!" });
    runtime.logger.error("api post runscript: Unauthorized");
    
  } else {
    // [3] Forward the script for execution via `runScript`
    runtime.scriptsMgr.runScript(script, req.body.params.toLogEvent).then(function (result) {
      res.json(result);
    }).catch(function (err) {
      if (err.code) {
        res.status(400).json({ error: err.code, message: err.message });
        runtime.logger.error("api post runscript: " + err.message);
      } else {
        res.status(400).json({ error: "unexpected_error", message: err.toString() });
        runtime.logger.error("api post runscript: " + err);
      }
    });
  }
});

This function fetches the supplied script object from the POST body (at [1]), and then begins checking whether the user is authorized to execute the requested script (at [2]) via the call to isAuthorised. If the authorization check succeeds the script is then executed by runtime.scriptsMgr.runScript (at [3]). These authorization and execution helper functions, from server/runtime/scripts/index.js and server/runtime/scripts/msm.js, are included below.

this.isAuthorised = function (_script, permission) {
  // [4] Call `getScript` (below), which identifies the script by `id`
  const st = scriptModule.getScript(_script);
  var admin = (permission === -1 || permission === 255) ? true : false;
  if (runtime.settings.userRole) {
    admin = admin || permission?.groups === -1 || permission?.groups === 255;
    if (admin) {
      return true;
    }
    if (!st.permissionRoles || !st.permissionRoles.enabled) {
      return true;
    }
    if (permission && permission.info && permission.info.roles) {
      // [5] Return true if the user is permitted to run the script identified earlier, by `id`
      return st.permissionRoles.enabled.length <= 0 || permission.info.roles.some(role => st.permissionRoles.enabled.includes(role));
    }
  } else if (admin || (st && (!st.permission || st.permission & permission))) {
    return true;
  }
  return false;
}

this.getScript = function (_script) {
  // [6] Fetch the internal script object from `scriptsMap` using `_scripts.id`
  return Object.values(scriptsMap).find(s => s.id === _script.id);
}
  
this.runScript = function (_script) {
  if (scriptsModule) {
    var paramValues = _script.parameters.map(p => utils.isNullOrUndefined(p.value) ? p : p.value);
    // [7] Defer to the user-provided `name` attribute, if available, falling back to `id` only if `name` is missing
    if (!_script.name) {
      _script = Object.values(scriptsMap).find(s => s.id === _script.id);
    }
    try {
      // [8] Execute the identified function
      return scriptsModule[_script.name](...paramValues);
    } catch (err) {
      console.error(err);
      return err;
    }
  }
}

As noted earlier, there is a mismatch in how these two functions map the user-supplied script object into the internal scriptsMap table vs the scriptsModule table. isAuthorized (at [4] and [5]) uses getScript for this mapping, which fetches the internal representation from scriptsMap using the supplied script.id attribute (at [6]). Execution, at [7] and [8], prefers to map the script against the scriptsMap table using the supplied script.name attribute, only falling back to the id lookup when name is absent, causing a mismatch between what is authorized and what is executed. A low-privilege user issuing a request to /api/runscript with script.id set to the identifier of a script for which they are authorized and script.name set to the name of a script for which they are not authorized can trick the server into executing the unpermitted script.

We note here that, for most HTTP endpoints, information related to these scripts is filtered server-side based on the requesting user’s privileges, so an unauthenticated or low-privileged user may not necessarily be informed of the existence of higher-privileged scripts (or their names). An attacker would either need to accurately guess a privileged script by name (perhaps by abusing the default naming convention of script_{#}), or abuse an information leak (such as TALOS-2026-2418) to recover unfiltered script names.

Assuming a FUXA instance has two or more differently privileged scripts, an unauthenticated or low-privilege user could use this authorization bypass to execute scripts for which they are not authorized. The final impact to the system is, of course, dependent on the content of scripts that have been deployed to the vulnerable instance, but could include unauthorized reads of runtime data, unauthorized writes to tags or device state, and execution of privileged internal operations.

Vendor Response (CVE-2026-42949)

Vendor Notes: The /api/runscript execution path now binds execution to the server-side script resolved by id, preventing a client-supplied name from changing the authorized execution target. This will be included in the next hotfix release.

Timeline

2026-05-29 - Cisco - Vendor Notification
2026-06-08 - Cisco - Follow-up sent
2026-06-08 - Frangoteam - Vendor Acknowledged
2026-06-10 - Cisco - Follow-up sent
2026-06-23 - Cisco - Follow-up sent
2026-06-23 - Frangoteam - Vendor replied
2026-07-16 - Cisco - Follow-up sent
2026-07-22 - Frangoteam - Vendor replied
2026-09-15 - Cisco - Follow-up sent

Credit

Matt Wiseman of Cisco Talos