GitHub Security Advisory: GHSA-gc9w-cc93-rjv8
Package: froxlor/froxlor (COMPOSER)
Severity: CRITICAL (CVSS 9.1)
CWE: CWE-94: Improper Control of Generation of Code ('Code Injection')
Vulnerable versions: <= 2.3.5
Patched in: 2.3.6

Summary: Froxlor has a PHP Code Injection via Unescaped Single Quotes in userdata.inc.php Generation (MysqlServer API)

Details:
## 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.

## Details

The root cause is in `PhpHelper::parseArrayToString()` at `lib/Froxlor/PhpHelper.php:486`:

```php
// lib/Froxlor/PhpHelper.php:475-487
foreach ($array as $key => $value) {
    if (!is_array($value)) {
        if (is_bool($value)) {
            $str .= self::tabPrefix($depth, sprintf("'%s' => %s,\n", $key, $value ? 'true' : 'false'));
        } elseif (is_int($value)) {
            $str .= self::tabPrefix($depth, "'{$key}' => $value,\n");
        } else {
            if ($key == 'password') {
                // special case for passwords (nowdoc)
                $str .= self::tabPrefix($depth, "'{$key}' => <<<'EOT'\n{$value}\nEOT,\n");
            } else {
                // VULNERABLE: $value interpolated without escaping single quotes
                $str .= self::tabPrefix($depth, "'{$key}' => '{$value}',\n");
            }
        }
    }
}
```

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`):

1. `validateAccess()` (line 82) checks the caller is an admin with `change_serversettings`
2. `privileged_user` is read via `getParam()` at line 88 with **no validation** applied
3. `mysql_ca` is also read with no validation at line 86
4. The values are placed into the `$sql_root` array at lines 150-160
5. `generateNewUserData()` is called at line 162, which calls `PhpHelper::parseArrayToPhpFile()` → `parseArrayToString()`
6. The result is written to `lib/userdata.inc.php` via `file_put_contents()` (line 548)
7. Setting `test_connection=0` (line 92, 110) skips the PDO connection test, so no valid MySQL credentials are needed

The generated `userdata.inc.php` is loaded on **every request** via `Database::getDB()` at `lib/Froxlor/Database/Database.php:431`:

```php
require Froxlor::getInstallDir() . "/lib/userdata.inc.php";
```

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**

```bash
curl -s -X POST https://froxlor.example/api.php \
  -u 'ADMIN_APIKEY:ADMIN_APISECRET' \
  -H 'Content-Type: application/json' \
  -d '{