How to identify privilege escalation in SaaS apps
To find privilege escalation, sign in as each role, record the requests the app makes, and replay them with a lower-privileged session or another tenant’s IDs. Any request that still succeeds is an escalation. Most findings in SaaS apps come from three places: role fields the user can edit, admin checks that exist only in the UI, and object IDs that are never matched to the caller’s organization.
Vertical vs horizontal privilege escalation
| Type | What happens | Example |
|---|---|---|
| Vertical | A user gains a higher role’s rights | A member calls the admin-only “delete workspace” endpoint |
| Horizontal | A user reaches another user’s or tenant’s data at the same role | A user changes org_id in a request and reads another company’s invoices |
OWASP groups both under Broken Access Control, A01 in its 2021 Top 10.
Step 1: Build a role matrix
List every role (owner, admin, member, viewer, unauthenticated) and every action that changes or returns data. Mark which roles are allowed each action. Create two test accounts per role, in two separate organizations. You need the second organization to test tenant isolation.
Step 2: Record and replay requests
- Sign in as the highest role and perform each action. Save the requests from the browser’s network tab or a proxy such as OWASP ZAP or Burp Suite.
- Replay each request with a lower role’s session token.
- Replay it again with a user from the other organization.
- Replay it with no session at all.
Compare the results with the matrix. A 200 where the matrix says “denied” is a finding, even if the UI hid the button.
Step 3: Check where roles are assigned
These paths decide who gets which role. Test each one directly:
- Profile update endpoints. Send
role: "admin"oris_admin: truein the body of a normal profile update. If the server copies the whole body into the record, the user just promoted themselves. This is mass assignment. - User-editable metadata. In Supabase,
user_metadatacan be changed by the signed-in user withupdateUser. A role stored there is a role the user chooses. Store roles inapp_metadataor a table only the backend writes. - Invite and join flows. Accept an invite, then change the role or organization ID in the accept request. Reuse an invite link after it was revoked.
- Organization switching. Change the active
org_idin a header, cookie or request body to an organization you do not belong to.
Step 4: Check object-level access
For each endpoint that takes an ID, request an ID that belongs to the other organization. Lists, exports, file downloads and webhooks are often missed. Our BOLA pattern write-up shows how generated CRUD code skips the ownership check.
If the backend is Supabase, test the database directly with the public key as well. RLS policies that check only auth.uid() is not null let any signed-in user read every tenant’s rows. The anon key guide covers that test.
Common findings in AI-generated SaaS apps
| Finding | Where to look |
|---|---|
| Admin check only in the React component | Call the API route directly without the admin role |
Role read from a request field or from user_metadata in the JWT |
Edit the field or metadata and resend |
org_id taken from the request body |
Swap in another organization’s ID |
security definer database function with no role check |
Call it via RPC as a normal user |
| Export or report endpoint without tenant filter | Request a report as a member of another organization |
Fixes
- Enforce every permission check on the server, keyed to the session, not to request fields.
- Derive the organization from the session or a membership lookup, never from the client.
- Write roles only from backend code, and reject unknown fields on update endpoints.
- Add an automated test per row of the role matrix so a regression fails the build.
Reviewed against OWASP and Supabase docs on October 9, 2026.
Test every role against every route
Our agent signs in as each role you give it and replays requests across roles and tenants on your deployed app.
14-day free trial · No credit card · Cancel anytime