The Signup Form That Made Me an Admin
A signup form trusted a client-supplied role and copied it into permissions, minting admins. Here is how we found it on a fintech and how to close it.
The setup
The target was an AI-accounting fintech aimed at freelancers and small companies - the kind of product that connects to your bank, reads your transactions, and helps with bookkeeping and tax. That means it holds two of the most sensitive categories a small business has: bank-linked financial data and tax information. A product like this earns trust by being boring and correct, and it handles data where a single privilege mistake is a serious one.
We were interested in the front door: registration. A self-serve fintech has to let anyone sign up, which makes the signup handler one of the few pieces of privileged server logic that is exposed to completely anonymous callers by design. If something there trusts the wrong input, the barrier to abuse is zero.
Recon
Working black-box, with no account, we mapped the external surface and then focused on the registration flow. We watched what the signup form actually sent to the server - the full shape of the payload, not just the fields the form drew on screen. Forms show a handful of inputs; the request underneath often carries more, and the server decides what to trust.
Alongside the registration flow, the recon turned up two other things worth noting. Some of the company’s internal automation tooling was publicly exposed rather than kept behind the network boundary. And the managed authentication provider behind the product had email auto-confirm switched on, meaning a newly registered account was treated as verified without the user ever proving they controlled the email address. Neither is a critical finding on its own. Both matter for how far the main finding reaches.
The hypothesis
Registration handlers are a classic place for over-trust. The reasoning is structural: a signup endpoint has to accept a payload from an anonymous user and turn it into a real account, which means it is writing user-controlled input into a privileged data structure. If the handler copies the whole payload into the account record instead of picking out only the fields it means to accept, then any extra field the client adds rides along.
So the hypothesis was that this registration handler would trust client-supplied fields it should have ignored - and that the most valuable field to try was the one naming the account’s role. If the server took a role from the client, a user could name their own privileges.
What we tried
We registered an account and added a field to the payload that the form never showed - a role value naming an elevated role. Then we looked at what the account came back as.
It came back as an admin.
To understand the mechanism rather than just the result, we looked at where the promotion happened. The role we supplied was not being sanitized and dropped. It was being copied. A database trigger took the client-supplied role from the signup data and wrote it into the account’s role metadata. The trigger was a convenience - a way to carry a value from registration into the permissions model in one step - and it carried whatever the client sent, without an allow-list deciding which values were legitimate.
The two recon notes compounded it. Email auto-confirm meant the elevated account was immediately usable, with no verification step to slow a would-be attacker. The exposed internal automation tooling meant an admin account had more reach than it should. The single mass-assignment gap was the escalation; the environment around it removed the friction.
We verified the mechanism on a single throwaway account, confirmed the promotion, and cleaned it up - the account was removed once the behavior was established. We did not use the elevated access to read other users’ data or touch the exposed tooling.
One part of the surface held up well, and it is worth saying so. A separate stateless token server we tested behaved soundly - it validated correctly and did not exhibit the trust-the-client weakness. This was not a system that was careless everywhere. It was a specific, well-understood gap in one handler.
What we found
The finding was a signup that let a user assign their own privileges.
POST <registration endpoint>
{
"email": "<throwaway>",
"password": "<...>",
"role": "<elevated role>" <- extra field, trusted
}
-> account created
-> DB trigger copies submitted "role" into account role metadata
-> account is now admin, and (email auto-confirm) immediately active
The chain, stated plainly:
- The client stated its own role, and the server believed it. The registration payload carried a role field the form never exposed, and the handler passed it through instead of ignoring it.
- A database trigger did the promotion. The elevation was not in obvious application code. A convenience trigger copied the client-supplied role into the account’s role metadata, with no allow-list gating which values were acceptable.
- The environment removed the friction. Email auto-confirm made the new admin account instantly usable without proving control of the email, and internal automation tooling was publicly exposed, widening what an admin could reach.
Why it happens
Nobody decided to let users make themselves admins. This is what a convenience trigger and a trusted client add up to.
The database trigger exists for a good reason: carrying a value from signup into the permissions model automatically saves boilerplate and keeps the two in sync. The gap is that it copies the value without an allow-list - it trusts that whatever arrives in the role field is legitimate, because in normal use the front end only ever sends the ordinary value. The front end is not the only thing that can send the request, though. Under deadline, “the client only sends valid roles” quietly becomes “the client can send any role,” and the trigger honors all of them equally.
The managed auth provider’s auto-confirm setting is a similar story: it is a convenience that speeds up onboarding during development, and it is easy to leave on. On its own it is minor. Combined with a signup that mints admins, it means the admin account is real and usable the instant it is created.
For developers
If you own a registration flow, four controls close this:
- The server assigns roles, never the client. A new account’s role is a decision your backend makes, not a value it reads from the payload. Set it server-side to a fixed default and ignore any role the client sends.
- Reject unexpected fields. Validate the signup payload against an explicit allow-list of the fields you accept. Anything else - a role, a permission flag, an internal identifier - is dropped, not passed through. This closes mass assignment at the door, and it closes the trigger behind the door too, because the illegitimate value never reaches it.
- Default to least privilege. Every new account starts with the minimum rights. Elevation is a deliberate, authenticated, audited action taken by someone who already has the authority - never a byproduct of registration.
- Do not expose internal automation tooling, and require real email verification. Keep internal tooling behind your network boundary, and turn off auto-confirm so an account has to prove control of its email before it is usable. Neither is the escalation, but both are the difference between a contained gap and a usable attack.
And one detection step: audit your accounts for role assignments that did not come from your own promotion path. An admin that no administrator created is the fingerprint of this bug.
The takeaway
When the client is trusted to state its own role, registration becomes a self-service privilege desk. The escalation here was a single mass-assignment gap - a convenience trigger that copied a client-supplied role without an allow-list - and the environment around it, auto-confirmed email and exposed internal tooling, turned a contained mistake into a usable one. The fix is a one-line principle: the server assigns roles, the client never does.
This teardown is one instance of a pattern we see repeatedly. We wrote up the general case, and how to defend against it, in When New Accounts Are Born as Admin.
Frequently asked
What is a mass-assignment vulnerability?
How does a user self-assign admin at signup?
How do you prevent privilege escalation at registration?
This is one finding from a harness that runs continuously. See how Greywatch finds, proves, and fixes them.
How it works →

