Home/Journal/Teardown
TeardownCriticalauthorizationregistrationprivilege-escalation

New Accounts Born as Executives

A student-housing CRM handed every new signup the top executive role. Here is how we found it, why it happened, and the controls that stop it.

Ajay Kumar··8 min read·a student-housing CRMImproper privilege assignment on registration (CWE-266/862)

The setup

The target was a student-housing and co-living booking company - the kind of operator that runs a portfolio of managed residences and fills them through a sales team working inbound leads. Behind the public booking site sits the part that actually runs the business: an internal lead CRM where the sales pipeline lives, where staff records sit, and where the operators whose properties are listed are tracked.

That internal CRM was what interested us. A booking company’s public site is a brochure. The CRM is the crown jewels: who is buying, who is selling, what every property is worth, and the people who work the accounts. If a black-box attacker can reach that, the leak is not a marketing inconvenience, it is the commercial core of the company.

Recon

Working black-box, from the open internet with no account, we enumerated the company’s web surface and looked at how the internal application authenticated its users. Most of the pipeline was gated behind a login, as expected.

The detail that stood out was the login page itself: it offered self-registration. An internal sales tool that lets a visitor create their own account is unusual. Staff tools are normally provisioned by an administrator, not opened for public signup. So the open “register” link was the first thread worth pulling.

The hypothesis

Open self-registration is not automatically a vulnerability. It becomes one when you combine it with a second decision: the role a brand-new account is born with.

Every registration flow has to assign the new user some default role. If that default is “read nothing until an admin approves you,” open signup is merely untidy. If the default is high - a role wired up during early development so the first internal users could see everything - then open signup is a door into the application at whatever level that default grants.

So the hypothesis was narrow: register a fresh account and check what role it lands with. If the default was anything above the floor, the open signup was not a nuisance, it was the finding.

What we tried

We registered a brand-new account through the public signup flow, using an address we controlled, and logged in.

The account did not land as a pending, zero-privilege user waiting for approval. It landed as the top executive role the CRM defined - the “CXO” tier - with full read of the sales pipeline and the staff records. A stranger who had existed for less than a minute was holding the most privileged seat in the internal tool.

We then pulled on two adjacent threads. First, an unauthenticated brand key embedded in the client minted a valid backend token, and that token returned company data and property addresses for several of the operators the platform served. Second, the same token was accepted by the resident-facing application’s namespace - a cross-namespace token confusion, where a credential issued for one context was honored in another it was never scoped to.

What we deliberately did not do matters here. Everything we touched was read-only. We confirmed the access, recorded what the role could see, and retained none of the data. On a re-test several days later, the registration path and the executive default were still open - so this was not a transient misconfiguration we happened to catch mid-deploy.

What we found

The mechanism was configuration, not a clever exploit, and one detail made that unmistakable.

Registering any new account auto-granted the top executive (“CXO”) role on the internal lead CRM. That single default gave full read of the sales pipeline and the staff directory:

-> POST <registration endpoint>
   { "email": "<attacker-controlled>", "password": "<...>", ... }

<- 200 OK  { "user": { "role": "CXO", ... } }

-> GET <pipeline endpoint>   (with the new account's session)
<- 200 OK  { "leads": [ ... full sales pipeline ... ],
             "staff": [ ... internal staff records ... ] }

Compounding it, an unauthenticated brand key minted a working backend token:

-> POST <token endpoint>   { "brand_key": "<key from client source>" }
<- 200 OK  { "token": "<valid backend JWT>" }

   ...token returns operator company data and property addresses,
      and is ALSO accepted by the resident-app namespace...

The telling detail was a sibling application. The company ran a second app on the identical codebase, and that sibling shipped with registration disabled. Same code, different switch. The vulnerability was not in the code that both apps shared - it was in the deployment configuration of this one, where a setting that should have been off was left on.

Why it happens

Nobody wired an executive default into a signup form on purpose. This is what early-stage velocity leaves behind.

When an internal tool is first built, the fastest way to make it usable is to give the first accounts full access - the founding team needs to see everything, and role granularity is a later problem. So the default role gets set high, and self-registration is left enabled so early teammates can onboard themselves without waiting on an admin. Both are reasonable during week one.

The gap opens when those two convenience settings survive into a live deployment and never get walked back. The sibling app proves the fix was known: someone disabled registration there. It just was not applied uniformly. A shared codebase inherits its danger from whichever deployment forgot to flip the switch, and the secure configuration was the exception, not the default.

For developers

If you run an internal application with any form of self-service signup, three controls close this:

  1. Default new accounts to the lowest privilege. A freshly registered account should see nothing of value until a human with authority elevates it. The default role is a security decision, not a convenience default - treat “born with no access” as the only acceptable starting state.
  2. Never let self-registration reach an internal CRM. Staff tools should be provisioned by an administrator, invite-only at most. If a visitor from the open internet can create a working account on your sales pipeline, the login page is the vulnerability regardless of what role it grants.
  3. Make the secure configuration the default across every deployment. When one codebase powers several apps, the safe setting has to be baked in, not switched on per-deployment. If one app has registration disabled and another does not, that difference is a live bug waiting to be found. Audit every deployment of a shared build for the same posture.

And one detection step: review your registration logs for accounts created outside your own onboarding, and audit which role each new account was assigned at creation. An executive account that no admin provisioned is the signal.

The takeaway

A signup form is an authorization decision, not a form. The role a new account is born with is the whole game: if the default is high and the door is open, a stranger becomes an executive in one request. This is a configuration flaw, which makes it both easy to ship and easy to fix - the danger is that it hides in a setting nobody re-checks after launch, and it hides differently in every deployment of the same code.

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 privilege assignment vulnerability on signup?
It is a flaw where the registration flow grants a new account a role with more access than a new, untrusted user should ever get. If the default role is high, a stranger who registers lands inside the application with privileges meant for staff or administrators.
Why does open self-registration on an internal tool matter?
Internal tools like a sales CRM assume every user is a vetted employee. If self-registration is reachable from the internet and assigns a working role, that assumption breaks: anyone can mint themselves an account and read whatever that role can see, no insider access required.
How do you prevent new accounts from getting admin rights?
Default every new account to the lowest privilege, require an existing administrator to elevate roles, and never let self-registration reach an internal application. Make the secure configuration the default across every deployment of a shared codebase.

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

How it works →