GHSA-gc9w-cc93-rjv8: Froxlor has a PHP Code Injection via Unescaped Single Quotes in userdata.inc.php Generation (MysqlServer API)
Summary`PhpHelper::parseArrayToString()` writes string values into single-quoted PHP string literals without escaping single quotes. When an admin with `change_serversettings` permission adds or updates a MySQL server via the API, the `privileged_user` parameter (which has no input validation) is written unescaped into `lib/userdata.inc.php`. Since this file is `require`d on every request via `Database::getDB()`, an attacker can inject arbitrary PHP code that executes as the web server user on every subsequent page load.DetailsThe root cause is in `PhpHelper::parseArrayToString()` at `lib/Froxlor/PhpHelper.php:486`:Note that the `password` key receives special treatment via nowdoc syntax (line 484), which is safe because nowdoc does not interpret any escape sequences or variable interpolation. However, all other string keys — including `user`, `caption`, and `caFile` — are written directly into single-quoted PHP string literals with no escaping.The attack path through `MysqlServer::add()` (`lib/Froxlor/Api/Commands/MysqlServer.php:80`):`validateAccess()` (line 82) checks the caller is an admin with `change_serversettings``privileged_user` is read via `getParam()` at line 88 with **no validation** applied`mysql_ca` is also read with no validation at line 86The values are placed into the `$sql_root` array at lines 150-160`generateNewUserData()` is called at line 162, which calls `PhpHelper::parseArrayToPhpFile()` → `parseArrayToString()`The result is written to `lib/userdata.inc.php` via `file_put_contents()` (line 548)Setting `test_connection=0` (line 92, 110) skips the PDO connection test, so no valid MySQL credentials are neededThe generated `userdata.inc.php` is loaded on **every request** via `Database::getDB()` at `lib/Froxlor/Database/Database.php:431`:The `MysqlServer::update()` method (line 337) has the identical vulnerability with `privileged_user` at line 387.PoC**Step 1: Inject PHP code via MysqlServer.add API**This writes the following into `lib/userdata.inc.php`:**Step 2: Trigger code execution**Any subsequent HTTP request to the Froxlor panel triggers `Database::getDB()`, which `require`s `userdata.inc.php`, executing `system("id")` as the web server user:The `id` output will appear in the response (or can be captured via out-of-band methods for blind execution).**Step 3: Cleanup (attacker would also clean up)**The injected code runs on every request until `userdata.inc.php` is regenerated or manually fixed.ImpactAn admin with `change_serversettings` permission can escalate to **arbitrary OS command execution** as the web server user. This represents a scope change from the Froxlor application boundary to the underlying operating system:**Full server compromise**: Execute arbitrary commands as the web server user (typically `www-data`)**Data exfiltration**: Read all hosted customer data, databases credentials, TLS private keys**Lateral movement**: Access all MySQL databases using credentials stored in `userdata.inc.php`**Persistent backdoor**: The injected code executes on every request, providing persistent access**Denial of service**: Malformed PHP in `userdata.inc.php` can break the entire panelThe `description` field (validated with `REGEX_DESC_TEXT = /^[^\0\r\n<>]*$/`) and `mysql_ca` field (no validation) are also injectable vectors through the same code path.Recommended FixEscape single quotes in `PhpHelper::parseArrayToString()` before interpolating values into single-quoted PHP string literals. In single-quoted PHP strings, only `\'` and `\\` are interpreted, so both must be escaped:Alternatively, use the same nowdoc syntax already used for passwords for all string values, which provides complete injection safety:Additionally, consider adding input validation to `privileged_user` and `mysql_ca` in `MysqlServer::add()` and `MysqlServer::update()` as defense-in-depth.
CVSS v3.1: 9.1