All vulnerabilities
GHSA-pp2f-frcg-w8rv

froxlor/froxlor: Missing authorization via unchecked `parentdomainid` in `SubDomains.delete`, enabling deletion of a reseller-assigned main domain

froxlor/froxlor
CVSS
5.4Medium
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in2.3.15Fixed versionGitHub
Published20 Oct 2026Updated 20 Oct 2026GitHub

What fixes it

  • 2.3.15froxlor/froxlor, composerGitHubcomposer require "froxlor/froxlor:^2.3.15"

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

Summary

**Missing authorization (CWE-862) in SubDomains::delete: the command never checks parentdomainid, so a customer deletes a main domain its reseller assigned to it.** The guard that is supposed to stop this exists only in the presentation layer, and SubDomains::get deliberately returns main domains, so the row the customer hands to delete can be one. The delete also skips every bookkeeping step its admin-only sibling performs, which leaks the reseller's domain slot permanently and drives the customer's own subdomains_used counter negative.

Details

SubDomains::delete (lib/Froxlor/Api/Commands/SubDomains.php:1161-1211) checks caneditdomain at :1180 and never inspects parentdomainid. SubDomains::get (:493-497) has no parentdomainid restriction, so a parentdomainid = 0 main domain is returned to the customer and can be fed straight back.

The guard lives in the UI only — customer_domains.php:108-126, domains_cantdeletemaindomain.

caneditdomain is not a barrier either: for any domain added by a reseller (an admin with change_serversettings != 1) it is hard-coded to '1' in the else branch of Domains::add (Domains.php:535).

Compare the admin-only sibling Domains::delete (Domains.php:2255-2291), which deletes WHERE id = :id OR parentdomainid = :id, decrements the owning admin's domains_used, and resets panel_customers.standardsubdomain. SubDomains::delete does none of these: :1205-1211 deletes only the one row, so child subdomains are orphaned with a dangling parentdomainid; the reseller's domains_used is never decremented; and Customers::decreaseUsage(…, 'subdomains_used') at :1258 runs for a row that never counted toward that quota in the first place.

Proof of concept

froxlor 2.3.14 (c147de18), over HTTP. reseller1 (change_serversettings = 0) creates customer web2 and assigns it the main domain reseller-owned.example.test (id 4, parentdomainid = 0, caneditdomain = 1, adminid = 2). Customer web2 then deletes it with its own API key.

=== PRE ===
domain  4  reseller-owned.example.test  parentdomainid=0  adminid=2
admin   2  domains=-1  domains_used=1
cust    2  subdomains=5  subdomains_used=0

$ curl -u CUST2KEY:CUST2SECRET \
    -d '{"command":"SubDomains.delete","params":{"id":4}}' http://127.0.0.1/api.php
{ "data": { "id": 4, "domain": "reseller-owned.example.test", "adminid": 2, … } }

=== POST ===
domain-count 0                             <- the reseller's domain is gone
admin   2  domains=-1  domains_used=1      <- NOT decremented
cust    2  subdomains=5  subdomains_used=-1  <- counter driven NEGATIVE

The oracle is the row count and the two counters read back from the database after the call, not the HTTP status.

Impact

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:L — 5.4 Medium.

Customer → reseller. Destructive: the reseller loses a domain it provisioned, with the vhost, DNS and mail rows partly orphaned behind a dangling parentdomainid. And self-serve quota inflation: subdomains_used has no floor — ApiCommand.php:236-246 does a raw - 1 — so repeating the cycle grants the customer unbounded extra subdomain allowance.

Suggested fix

Reject parentdomainid = 0 rows in SubDomains::delete, the way customer_domains.php already does in the UI; and route main-domain deletion through Domains::delete, which does the cascade and the counter bookkeeping correctly.

What this is not

The deletion is still scoped to customerid = own (:1205), so a customer cannot delete another customer's domain — only one the reseller assigned to it.

The quota-denial consequence for the reseller was not demonstrated end to end. My reseller had domains = -1 (unlimited), so I proved the counter drift — domains_used not decremented, subdomains_used at -1 — and inferred, rather than showed, the allocation failure that follows on a finite quota.

Verified present at 2.3.14 and origin/v2 c147de18. Not present on origin/main (v3 rewrite).

Text from GitHub Advisory Database

Affected versions 1

  • froxlor/froxlor, composer>= 2.0.0, <= 2.3.14GitHub