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.
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:
- 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.
- 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.
- 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?
Why does open self-registration on an internal tool matter?
How do you prevent new accounts from getting admin rights?
This is one finding from a harness that runs continuously. See how Greywatch finds, proves, and fixes them.
How it works →

