Writable Tenant Attribute on Public Sign-Up Lets Anyone Pivot Into Any Customer's Account
The Risk
Anyone on the internet could create a free account and tell the platform "I belong to this customer's organization", and the platform believed them. From that single account an attacker could read the full employee directory of any customer (every name, email, role, plus the identity of the most senior administrator), plant fake teams that the real owners could not remove, and send invitations from inside the victim's own company to phish their own staff. One account could swap between every customer on the platform with a single API call.
The Vulnerability
The platform's login system was a cloud identity provider with a public sign-up app client. The app client's writable-attributes list included custom:aId, the tenant identifier the entire backend used for data scoping. Two flaws chained:
- Mass assignment on a privileged attribute. Both the unauthenticated
SignUpcall and the authenticatedUpdateUserAttributescall accepted attacker-supplied values forcustom:aIdand persisted them. The attribute was meant to be set server-side based on email domain or invitation token. - Missing membership check on the backend. Endpoints scoped by tenant ID read the value straight from the JWT claim. There was no row check confirming that the authenticated user actually had a record tied to that tenant.
What an attacker needed: one mailbox they controlled (any disposable address worked) and the victim's tenant ID, a UUID. That UUID leaked freely through any signed-in user's profile endpoint, through shared-resource permissions, or through the platform's domain-based auto-join feature when an attacker signed up using the victim's email domain.
The Attack
Step 1, sign up with the victim's tenant ID
The PoC posted to the cloud identity provider's SignUp endpoint with the victim tenant's UUID in the custom:aId attribute. The provider accepted it and emailed a verification code to the attacker's mailbox. The code was confirmed and a sign-in followed. The resulting access token decoded to:
{"sub": "<attacker-owned uuid>", "aId": "<victim tenant uuid>", "token_use": "access"} The attacker had no row in the platform's user table tied to that tenant. The token alone was enough.
Step 2, read the victim tenant's full user directory
A single GET /account/users call with the attacker's token returned every user in the victim tenant: email, first and last name, account role (including which user held the highest SUPER_ADMIN role), and creation timestamp. Adjacent endpoints returned the registered domain, enterprise status, and account-owner contact details.
Step 3, plant a persistent team and invite real victim users
A POST /team call from the attacker's token created a team whose accountId was the victim's, permanently anchoring the team to the victim tenant. The team showed up in the victim's workspace switcher, and a separate previously reported flaw in team-management role hierarchy meant the victim's senior administrator could not evict the attacker. A subsequent POST /teams/{id}/invitations issued real same-tenant invitations to real victim users, who saw "you have been invited to a team in your own organization" inside the legitimate web UI.
Step 4, hot-swap to the next customer
One UpdateUserAttributes call with the next victim tenant's UUID, then a fresh sign-in, produced a token scoped to that tenant. Re-running step 2 dumped the second customer's directory. One attacker-owned account pivoted into every customer on the platform with no re-registration and no per-victim mailbox.
The Impact
As an unauthenticated internet attacker, after a single self-registration, it was possible to:
- Read the full employee directory of any customer whose tenant ID was discoverable. For corporate customers this is the full list of platform-using employees plus the highest-value spear-phishing target in the org chart.
- Read tenant metadata: registered domain, enterprise status, account-owner contact details.
- Plant teams permanently inside the victim tenant and send same-tenant invitations to real victim users from inside their own organization, a high-credibility spear-phishing primitive.
- Upload content owned by the victim's tenant ID, polluting cross-tenant analytics, rollups, and audit logs.
- Hot-swap between every customer on the platform with one account, no re-registration, no password reset, no per-victim mailbox.
Remediation
- Remove the tenant-ID attribute from the writable list on the public sign-up app client. This blocks both the sign-up call and the user attribute update call from setting it from a public client.
- Set the tenant-ID attribute server-side in a pre-sign-up or pre-token-generation hook on the user pool. Derive it from the user's email domain (using the existing tenant-domain mapping the auto-join feature already uses) or from a server-issued invitation token.
- Verify a
(user-id, tenant-id)membership row in the backend on every endpoint that scopes data by the JWT tenant claim. Reject any request where no user-table row exists for that pair. - Audit existing accounts in the user pool for entries whose tenant-ID does not match the value the legitimate flow would assign. Such accounts are likely attackers; revoke their refresh tokens and disable them.
- Verify the equivalent configuration on the production environment of the same product. Writable-attribute lists are typically set declaratively from shared infrastructure modules and replicate across environments.