Mass Assignment Hidden in Plain Sight
This one didn't need any tooling. No fuzzer, no binary, no clever timing trick. It needed reading a request body and asking a single question: what happens to every field in here, not just the ones the docs mention?
Reading the request the app actually sees
Public registration forms are a good place to look for this class of bug, because they're the one part of an app that unauthenticated users get to talk to directly, and they're often wired up quickly, early in a project's life, and never revisited. FOSSBilling's guest registration endpoint takes a JSON body: email, password, name, the fields you'd expect on any signup form.
I wasn't looking at the documented fields. I was looking at what the handler does with the whole $data array once it has it.
The gap between "documented" and "accepted"
The registration handler passes the entire request body into the internal client-creation method. Nothing strips it down to an allow-list first. Buried among the legitimate fields, that method also happily accepts group_id, a value that's supposed to be something only an admin assigns after reviewing an account, since client groups gate access to discounted pricing and promo codes.
group_id as a registration parameter. The code doesn't care what the documentation says.That's the whole bug. A field that was never meant to be user-controlled was reachable anyway, because the validation logic trusted a list of what it expected to receive instead of a list of what it was willing to accept.
One curl command
Proving it out took a single request:
curl -X POST https://target/api/guest/client/create \
-H "Content-Type: application/json" \
-d '{"email":"x@example.com","password":"...","group_id":3}'
No authentication, no prior knowledge beyond a group ID you can usually infer or enumerate. The account comes back a member of whatever group you named, discounts and all, and the server never asked whether that was allowed.
Why this pattern keeps showing up
Mass assignment bugs are unglamorous, which is probably why they get underreported relative to how often they occur. There's no exploit chain to diagram, no memory corruption, nothing that looks impressive in a screenshot. But the underlying mistake, trusting an entire object instead of the specific fields a caller should be allowed to set, shows up constantly in frameworks that make it easy to bind a request body straight onto a model. It's a design habit, not a one-off typo, which is exactly why I check for it on every API I review, not just the ones that look interesting.
The fix, and the actual lesson
FOSSBilling's patch explicitly strips admin-only fields before the data reaches client creation. That's the correct direction of trust: allow-list what a public endpoint accepts, don't block-list what you're hoping it won't receive. The second approach only works until someone reads the code instead of the docs.