Skip to main content
VulnerabilityPublished

froxlor/froxlor · Froxlor customer can create MySQL databases on disallowed servers via Mysqls.add API

Affects froxlor/froxlorSource GCVE Database

0

CVSS 4.3 Medium : upgrade to 2.3.7 when convenient.

Data as of 3 Jul 2026, 22:49 UTC

CVSS 3.1 base score
4.3Medium
How severe the flaw is on the 0–10 CVSS scale.
Patch
Available – fixed in 2.3.7
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N

What to do

Upgrade froxlor/froxlor to 2.3.7.

Affected software

ProductAffectedFixed in
Packagistfroxlor/froxlorAffected
  • <2.3.7
Fixed in
  • 2.3.7

Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.

AI-suggested

A fix is available in Froxlor version 2.3.7 and later. Users should upgrade to version 2.3.7 or newer to ensure the mysql_server parameter is properly validated against the customer's allowed_mysqlserver list. Until patched, restrict API access to trusted customers only and monitor for unauthorized database creation attempts. Patch status is not explicitly stated in the input but the affectedVersions field indicates versions before 2.3.7 are vulnerable, implying 2.3.7 contains the fix.

Description

## Summary The `Mysqls.add` API command (`lib/Froxlor/Api/Commands/Mysqls.php`) accepts a customer-controlled `mysql_server` parameter and only validates that the value is numeric and that the server index exists in `userdata.inc.php`. It never checks the value against the calling customer's `allowed_mysqlserver` allowlist. A customer can therefore create a database, plus a MySQL user with a password they choose, on any MySQL server the operator has configured — including servers that were explicitly excluded from that customer (e.g. a separate cluster, premium-tier host, or another tenant pool). The same `allowed_mysqlserver` check is correctly enforced in `MysqlServer::get()` / `MysqlServer::listing()` and in the customer-facing UI (`customer_mysql.php`), confirming the omission is a bug, not by-design. ## Details **Vulnerable code path** — `lib/Froxlor/Api/Commands/Mysqls.php:69-99` (`add()`): ```php public function add() { if (($this->getUserDetail('mysqls_used') < $this->getUserDetail('mysqls') || ...) { ... $customer = $this->getCustomerData('mysqls'); // line 80 $dbserver = $this->getParam('mysql_server', true, // line 81 — user-controlled $this->getDefaultMySqlServer($customer)); ... $dbserver = Validate::validate($dbserver, ..., '/^[0-9]+$/', ...); // line 92 — numeric only Database::needRoot(true, $dbserver, false); // line 93 — root ctx for ANY index Database::needSqlData(); $sql_root = Database::getSqlData(); Database::needRoot(false); if (!is_array($sql_root)) { // line 97 — only existence check throw new Exception("Database server with index #" . $dbserver . " is unknown", 404); } ... $username = $dbm->createDatabase($newdb_params['loginname'], $password, $dbserver, ...); // line 116/118 — DB+user created ... Database::pexecute($stmt, ["customerid"=>$customer['customerid'], ..., "dbserver"=>$dbserver], ...); } } ``` The `$customer['allowed_mysqlserver']` field IS read on line 80 but is only consumed by `getDefaultMySqlServer()` (lines 566-573) to compute a default when the request omits `mysql_server`. As soon as the client supplies the parameter, the default path is skipped and no further authorization gate runs. **Cross-file evidence the check is intended elsewhere:** - `lib/Froxlor/Api/Commands/MysqlServer.php:319-323` — `get()` rejects with HTTP 405 when `$dbserver` is not in `allowed_mysqlserver`: ```php if ($this->isAdmin() == false) { $allowed_mysqls = json_decode($this->getUserDetail('allowed_mysqlserver'), true); if ($allowed_mysqls === false || empty($allowed_mysqls) || !in_array($dbserver, $allowed_mysqls)) { throw new Exception("You cannot access this resource", 405); } ... } ``` - `lib/Froxlor/Api/Commands/MysqlServer.php:252-257` — same allowlist filter on `listing()`. - `customer_mysql.php:222` — UI rejects with `Response::dynamicError('No permission')` when `empty($allowed_mysqlservers)`. **Chain of execution (attacker → impact):** 1. Customer authenticates to `api.php` with apikey/secret. The only API gate is `cust_api_allowed`; `allowed_mysqlserver` is not consulted at auth time. 2. Customer sends JSON `{"command":"Mysqls.add","params":{"mysql_password":"<valid>","mysql_server":<disallowed_idx>}}`. 3. `Mysqls.php:71` quota check passes (`mysqls_used < mysqls`). 4. `Mysqls.php:80` `getCustomerData('mysqls')` returns the caller's own row. 5. `Mysqls.php:81` `$dbserver` is set from the request (default-fallback path skipped). 6. `Mysqls.php:92` numeric regex passes. 7. `Mysqls.php:93-99` `Database::needRoot(true, $dbserver, false)` switches to the root context of the attacker-chosen server; existence check passes. 8. `Mysqls.php:116/118` `DbManager::createDatabase(...)` runs against the disallowed server using stored root credentials, creating the DB and granting the supplied password to `<loginname>_<sqlN>` (DbManager.php:177-218). 9. `Mysqls.php:127-141` inserts a row into `TABLE_PANEL_DATABASES` with the attacker's `customerid` and the disallowed `dbserver`, allowing later management via `Mysqls.get/update/delete` (which only filter by `customerid` for non-admins, e.g. `Mysqls.php:282`). ## PoC Preconditions on the target instance: - ≥2 MySQL servers configured in `lib/userdata.inc.php` (e.g. index 0 default, index 1 internal/premium). - Customer X with `allowed_mysqlserver=[0]`, `cust_api_allowed=1`, `mysqls > 0`, and an issued API key (`apikey:secret`). Request — customer creates a database on server `1`, which is *not* in their allowlist: ```bash curl -k -u 'CUST_APIKEY:CUST_SECRET' \ -H 'Content-Type: application/json' \ -X POST \ -d '{"command":"Mysqls.add","params":{"mysql_password":"ValidP@ssw0

References

Community reviews

Loading reviews…