Home/Journal/Teardown
TeardownHighmass-assignmentprivilege-escalationauthentication

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.

Rahul Dharan··8 min read·an AI-accounting fintechMass assignment / privilege escalation (CWE-915)

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:

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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?
It is when an application copies client-supplied fields directly into a data object without an allow-list, so a caller can set fields the form never meant to expose. At signup, that lets a user submit a role or permission field and have the server trust it, self-assigning privileges they should never control.
How does a user self-assign admin at signup?
The registration payload includes an extra field naming a privileged role, and the server - or a database trigger behind it - copies that field into the account's permissions instead of ignoring it. The account is created with elevated rights because the client was trusted to state its own role.
How do you prevent privilege escalation at registration?
The server assigns roles, never the client. Reject or ignore unexpected fields in the signup payload, default every new account to least privilege, and enforce this in the data layer so no trigger or default silently promotes a user. Require real email verification so accounts cannot be created freely.

This is one finding from a harness that runs continuously. See how Greywatch finds, proves, and fixes them.

How it works →