Summary
Hiding a Signup Button Isn’t Security: From Client-Side Controls to Cross-Tenant Data Exposure Introduction A common security mistake is to assume that if a feature is hidden from the user interface, it is also disabled. During my bug bounty journey, I came across a similar scenario while testing a private program. The application did not show a Sign Up option on the login page. However, the frontend JavaScript contained a feature flag indicating that user signup was disabled: USER_SIGNUP: false At first glance, this looked like a reasonable security control. It wasn’t. The flag only affected the frontend. The backend API continued accepting registration requests directly. That discovery eventually led to a much larger attack chain: Hidden Signup ↓ Direct API Registration ↓ Confirmation Token Returned by API ↓ Email Verification Bypass ↓ Authenticated User ↓ Self-Service Staff Privilege Escalation ↓ Cross-Tenant User Enumeration ↓ Exposure of Tenant API Credentials The important lesson is simple: A security control implemented only in JavaScript is not an access-control mechanism.
- Starting with the Frontend The first interesting discovery came from inspecting the JavaScript files loaded by the login page. The application’s HTML referenced several JavaScript bundles, including: main-XXXX.js chunk-Qxxxx.js JavaScript bundle discovered from the login page The application loaded the JavaScript directly from the web server, meaning anyone visiting the application could retrieve it. Inside chunk-QR7xxxx.js , I found configuration similar to: apiDefaultClientId: “REDACTED_CLIENT_ID”, apiDefaultKey: “REDACTED_API_KEY”, apiExcludedPaths: [”^\/?config\/”], publicDomainUuid: “REDACTED_DOMAIN_UUID” The important observation wasn’t simply that configuration existed in JavaScript. The more interesting part was that the frontend contained information about the application’s API and its authentication model. Why is this important? Anything shipped to the browser should generally be treated as public. JavaScript can be:
- downloaded,
- inspected,
- modified,
- searched,
- copied,
- analyzed by anyone. Therefore, JavaScript should never be trusted to enforce authorization.
- The Signup Button Was Hidden — But the API Wasn’t The login page did not provide a visible registration option. Login page without a visible signup option However, instead of assuming that registration was completely disabled, I checked whether the application still exposed a signup route. The following URL was accessible: https://redacted.com/signup Direct access to the signup page This was the first important security observation. The application had effectively implemented: Frontend: “Don’t show signup” Backend: “Signup is still available” These are two completely different things. A malicious user does not have to use the application’s buttons. They can communicate directly with the backend API.
- Registering Directly Through the API Using an account controlled by me, I created a test account through the registration functionality. The relevant API request looked like this: POST /people/ HTTP/2 Host: api.redacted.com Content-Type: application/json { “email”: “attacker@example.com”, “first_name”: “Demo”, “last_name”: “User”, “password”: “REDACTED_PASSWORD”, “domain_uuid”: “REDACTED_DOMAIN_UUID” } The server responded with: HTTP/2 201 Created More importantly, the response contained a confirmation token. For example: { “uuid”: “REDACTED_UUID”, “email”: “attacker@example.com”, “confirm_token”: “REDACTED_CONFIRM_TOKEN” } Signup request and successful account creation Confirmation token returned by the API This was a significant finding. The application was supposed to require the user to confirm their email address. But the API had already returned the confirmation token required to complete the process. Although the signup API returned a confirm_token , using that token directly did not allow me to complete the account confirmation process. The confirmation flow was properly blocked on the server side, so I could not activate the newly created account simply by using the token returned during registration. At this point, I initially thought I had reached a dead end. However, the investigation did not stop there. While analyzing the JavaScript bundles loaded by the application, I noticed that the frontend contained the authentication logic and references to several backend API endpoints. This gave me a much clearer picture of how the application’s authentication flow worked. Instead of relying on the UI, I followed the API calls and authentication functions exposed in the JavaScript and continued testing the backend directly. This turned out to be the key to the next stage of the attack chain.
- The Email Verification Could Be Bypassed The frontend JavaScript revealed an endpoint responsible for confirming an account: POST /people/password/ The relevant code showed that the application used a confirmation token together with a password. The request looked like: POST /people/password/ HTTP/2 Host: api.redacted.com Content-Type: application/json { “confirm_token”: “REDACTED_CONFIRM_TOKEN”, “password”: “REDACTED_PASSWORD” } The server accepted the request and returned an authentication token. Example response: { “token”: “REDACTED_AUTH_TOKEN”, “person”: { “uuid”: “REDACTED_UUID”, “email”: “attacker@example.com”, “confirmed”: true } } Authentication token returned after confirmation Notice what happened. I never needed access to the mailbox associated with the account. The application effectively gave me everything required to confirm the account itself. The intended flow should have been: Signup ↓ Email sent to user ↓ User clicks confirmation link ↓ Account becomes confirmed ↓ User authenticates The observed flow was: Signup ↓ Confirmation token returned in API response ↓ Send a POST request to /people/password/ with confirm token and Password ↓ Account becomes confirmed ↓ Authentication token returned That is an email verification bypass.
- The Real Problem: I Could Change My Own Privileges At this point, I had a normal authenticated account. The next step was to understand what the authenticated API allowed me to modify. The frontend JavaScript showed a user-management endpoint: PUT /people/{uuid}/ The request accepted user attributes. I tested whether security-sensitive attributes were properly protected. The following request was sufficient: PUT /people/REDACTED_UUID/ HTTP/2 Host: api.redacted.com Authorization: Token REDACTED_AUTH_TOKEN Content-Type: application/json { “is_staff”: true } The server returned: HTTP/2 200 OK And the user object subsequently showed: { “is_staff”: true } Server response showing is_staff: true This was the turning point. A normal user should never be able to decide: “is_staff”: true for themselves. This is an example of a broken authorization / mass-assignment style vulnerability. The server was accepting a security-sensitive field from an unprivileged client without verifying whether the requester was authorized to change it.
- From Normal User to Staff After changing the account’s privilege level, I returned to the application. Get Manohar_K’s stories in your inbox Join Medium for free to get updates from this writer. The UI now exposed functionality that was previously unavailable. For example, the account could access the administrative functionality associated with staff users. Staff-level functionality becomes available The application also exposed an administrative section through the user interface. administration becomes accessible This demonstrates why the is_staff field was security-sensitive. It wasn’t simply changing a profile preference. It changed the authorization level of the account.
- Testing Tenant Isolation The application appeared to support multiple organizations or tenants. The administrative interface displayed organizations such as: Test Tenant 1 Tenant 2 … Organization management interface In a multi-tenant application, users from one organization should normally be prevented from accessing another organization’s users and data. I therefore tested whether the API correctly enforced tenant boundaries. The user-management endpoint accepted a domain_uuid parameter: GET /people/?domain_uuid=REDACTED_DOMAIN_UUID Using the newly obtained staff-level token, I requested information belonging to another tenant. GET /people/?domain_uuid=REDACTED_OTHER_TENANT_UUID&per_page=5 HTTP/2 Host: api.redacted.com Authorization: Token REDACTED_STAFF_TOKEN The server returned: HTTP/2 200 OK and returned users belonging to another organization. Cross-tenant user enumeration This confirmed that the privilege escalation was not isolated to the attacker’s own account. It could be used to cross an organizational boundary.
- The Most Serious Finding: Domain API Keys Were Exposed The final part of the attack chain was particularly serious. The application exposed a domain-management endpoint: GET /domains/ I sent: GET /domains/ HTTP/2 Host: api.redacted.com Authorization: Token REDACTED_STAFF_TOKEN The API returned information about multiple organizations. Among the returned information were fields such as: { “domain_uuid”: “REDACTED”, “client_id”: “REDACTED”, “api_keys”: [ { “api_key”: “REDACTED” } ] } /domains/ response exposing tenant information The response contained API credentials associated with multiple domains. This transformed the issue from: “A user can make themselves staff.” into: “An unauthenticated attacker can potentially progress to an account with access to credentials belonging to multiple organizations.” That distinction is extremely important when evaluating severity.
- Why This Became a Critical Chain Individually, some of the findings might appear less severe. For example: Hidden signup Not necessarily a vulnerability by itself. Public JavaScript configuration Not necessarily a vulnerability if the values are intentionally public. Signup API Not necessarily a vulnerability if public registration is intended. Confirmation token Potentially serious if it allows account verification without email ownership. is_staff modification High impact because it allows privilege escalation. Cross-tenant enumeration High impact because organizational boundaries can be crossed. API key exposure Potentially critical because credentials belonging to multiple organizations may be compromised. But when chained together: No Account ↓ Create Account ↓ Verify Account ↓ Obtain Authentication Token ↓ Become Staff ↓ Cross Tenant Boundary ↓ Access Tenant Credentials the overall impact becomes substantially greater.
- Impact The combined vulnerability chain could allow an unauthenticated attacker to:
- Create an account despite signup being disabled in the UI.
- Bypass the intended email-verification process.
- Obtain a valid authenticated session.
- Escalate their account to staff privileges.
- Access users belonging to other organizations.
- Cross tenant boundaries.
- Access sensitive organizational configuration.
- Retrieve API credential material associated with multiple tenants.
- Potentially access additional functionality available to staff users. During testing, the exposed data covered multiple organizations and included sensitive account and configuration information. The potential impact therefore extends beyond a single compromised user.
- Recommended Remediation
- Enforce authorization on the server Never rely on frontend flags such as: USER_SIGNUP: false The API itself must determine whether registration is allowed. For example: Frontend → hides signup Backend → rejects POST /people/ if public registration is disabled.
- Never return email-confirmation secrets in the signup response The confirmation token should be delivered through the intended verification channel. The server should not return something equivalent to: { “confirm_token”: ”…” } to an unverified client. Instead: Signup ↓ Generate random confirmation token ↓ Store securely server-side ↓ Send verification link by email ↓ User clicks link ↓ Account confirmed
- Protect security-sensitive fields Fields such as: is_staff is_superuser role permissions organization tenant should never be freely writable by normal users. A normal user request should not be able to change: { “is_staff”: true } The backend should either reject the field or perform an explicit authorization check.
- Enforce tenant isolation server-side Do not trust: domain_uuid organization_id tenant_id provided by the client. Instead, derive the user’s allowed tenant from the authenticated identity. Conceptually: Authenticated User ↓ Server determines allowed tenant ↓ Query restricted to that tenant rather than: Client supplies domain_uuid ↓ Server blindly trusts it
- Never expose API keys in general list endpoints An endpoint such as: GET /domains/ should not return reusable secrets. Instead, return only information required by the UI. For example: { “uuid”: “REDACTED”, “name”: “Example Organization” } Credential management should be separated behind strict authorization controls.
- Review every administrative endpoint The same authorization issue should be checked across all endpoints involving: Users Organizations Roles Permissions API keys SSO configuration Clients Domains Administrative settings Fixing only /people/{uuid}/ may leave similar privilege-escalation paths elsewhere.