Security Basics
10 min
A user should not be able to see another customer’s invoice merely by changing a value in the browser. Yet the answer to ‘can users bypass authorisation checks’ is often yes when a fast-moving SaaS relies on what the interface hides rather than what the server permits.
This is not usually a dramatic Hollywood-style breach. It is a missing ownership check on one API route, a Supabase policy that was never enabled, or an AI-generated admin condition that only exists in the React component. The result can still be serious: exposed customer records, unauthorised refunds, account changes, marketplace fraud, or access to data that makes a small incident very expensive.
For founders, the useful question is not whether your login screen works. It is whether every request reaching your backend is authorised for the specific user, tenant and action involved.
Can users bypass authorisation checks in a modern SaaS?
They can whenever the backend accepts a request without independently proving that the caller has the right to perform it. Authentication answers, ‘Who is making this request?’ Authorisation answers, ‘May this person access this exact resource or take this exact action?’ They are separate controls, and a valid session does not grant universal access.
A common failure is insecure direct object reference, usually shortened to IDOR. Imagine a customer portal that retrieves an invoice at /api/invoices/1234. The application may confirm that the caller is signed in, then return invoice 1234 without checking that it belongs to that caller’s organisation. The page might only show invoices from the current account, but the API has already made the real decision incorrectly.
Multi-tenant products are particularly exposed. If tenant boundaries are inferred from a client-supplied organisation ID, rather than established from the server-side session and checked against the record, a user may cross from one workspace into another. For a B2B SaaS business, that is often a reportable data exposure rather than a minor bug.
The same principle applies to privileged actions. A frontend button labelled ‘Approve refund’ is not a security control. Neither is a hidden admin route. If the relevant endpoint does not verify the user’s role and the allowed scope of the action, an unauthorised request may be accepted even though the button never appeared in the interface.
Where authorisation checks break under launch pressure
Rapid development tends to create gaps at the joins between your frontend, API, database and third-party services. AI-assisted coding can accelerate this pattern because generated code often completes the happy path convincingly while omitting the less visible checks around it.
Client-side checks mistaken for server-side protection
A frontend may decode a JWT, determine that a user is an administrator, and conditionally render an admin page. That is useful for the user experience, but it cannot enforce access. A user controls their browser, its requests and the values sent with them.
Every sensitive API route needs its own server-side decision. It should validate the session, establish the user identity from a verified token or trusted session, load the target resource, and confirm that the identity has the required relationship, role or permission. For write operations, it must also confirm that the requested state change is allowed.
Beware code that trusts fields such as userId, accountId, role or isAdmin from the request body. Those fields can be hints, but they must not be the source of truth.
Supabase RLS that exists in theory, not in production
Supabase row-level security is a strong control when policies match the data model and the application uses the correct keys. It becomes a liability when RLS is disabled, a table is excluded from policies, or the client has access to a service-role key.
A service-role key bypasses RLS by design. It belongs only in protected server-side environments, never in a browser bundle, public environment variable, mobile application or repository that contractors can access without need. If that key is exposed, an attacker may not need to find a weak policy at all.
Even with RLS enabled, policies need careful review. A policy that allows a user to select records where user_id = auth.uid() may protect personal data correctly, but a team-based table needs a membership check. A policy that uses a client-controlled column to establish tenancy can recreate the same IDOR risk at database level.
API routes that treat authentication as enough
Hosted authentication makes it easier to establish identity. It does not automatically apply your business rules. API routes for profile updates, exports, invitations, billing, refunds and workflow approvals need specific checks because each has a different impact.
Exports are a frequent blind spot. A user might be permitted to view a single record but not download an organisation-wide CSV containing every customer’s information. Similarly, support tooling may legitimately allow staff to view accounts while still requiring tighter controls for password resets, payment changes or impersonation.
The correct rule is rarely just ‘logged-in users can access this endpoint’. It is more often: ‘A verified member of this organisation, with this role, may perform this action on this resource under these conditions.’
Token and role handling that goes stale
JWT claims can be useful, but permissions embedded in tokens may become outdated when a user is removed from a workspace or downgraded from an admin role. Long-lived tokens increase that window. Unsafely accepting unsigned tokens, failing to validate the expected issuer or audience, or confusing decoded data with verified data creates more direct risk.
For high-impact actions, consider checking current membership or privilege server-side rather than relying solely on a claim issued hours ago. This adds a database lookup or cache decision, so there is a performance trade-off. For an invoice view, a short-lived, correctly validated claim may be sufficient. For a payout change or an organisation export, current authorisation is usually worth the extra work.
What a useful authorisation review actually tests
A real review does not stop at searching for the word auth. It maps the application’s assets, roles and trust boundaries, then tests whether the enforced rules match the intended product rules.
Start with the endpoints that expose or change valuable data: customer profiles, files, invoices, payment settings, invitations, API keys, admin functions and bulk exports. For each one, define who should be allowed to read, create, update or delete each resource. Include users from a different tenant, users with lower privilege and users whose access was recently revoked.
Then inspect the actual enforcement point. In a typical JavaScript stack, that may mean middleware, route handlers, server actions, database queries and Supabase RLS policies. The reviewer should trace the identity used at each layer and identify where a client-provided identifier replaces a trusted one.
Automated tools help find risky patterns, exposed credentials and known dependency issues. They do not reliably understand that an organisationId is supposed to match a membership record, or that a refund route needs a distinct approval rule. That is why manual testing and code review matter, especially in applications assembled quickly from generated snippets and examples.
Evidence should be specific. A useful finding identifies the route or file path, describes the affected role and resource, assigns a severity based on real impact, and states the corrective change. ‘Improve access control’ is not an action plan. ‘Add a server-side ownership check before returning invoiceId, and enforce a matching tenant policy in the database’ is.
Fix the decision point, not just the screen
The safest remediation is usually to centralise authorisation logic where practical, while keeping rules close enough to the affected resource that they cannot be skipped. Create reusable functions for common checks such as active organisation membership, resource ownership and administrator status. Call them from every sensitive route rather than copying slightly different conditions across the codebase.
At the database layer, enable RLS on tenant-scoped tables and write policies around verified identity and membership relationships. At the API layer, derive identity from the validated session, fetch the target record and compare its tenant or owner with the caller’s permitted scope. At the application layer, keep frontend visibility checks for usability, but never regard them as enforcement.
Do not forget non-code controls. Rotate any exposed service-role keys or API credentials, invalidate affected sessions where appropriate, review audit logs, and check whether unauthorised access may already have occurred. A fixed endpoint does not remove the consequences of a credential that has been public for months.
Regression tests are worthwhile once the immediate issue is resolved. Test the negative cases: a user from another workspace, a member without the required role, a deleted membership and a request with a substituted resource ID. These tests protect the fix when the next feature is built at speed.
Prioritise by what could happen next
Not every authorisation flaw carries the same urgency. A user accessing another user’s display preference is not equivalent to a user downloading all customer documents or changing banking details. Prioritise by the sensitivity and volume of reachable data, the ability to alter money or access, the ease of repetition, and whether the flaw crosses tenant boundaries.
However, low-severity findings can reveal a systemic pattern. One missing check in a profile route may indicate that dozens of routes were built with the same assumption: if the frontend does not display it, nobody can use it. Treat the first finding as a reason to examine the pattern, not just patch the individual line.
If you have launched with AI-generated routes, Supabase policies, hosted authentication and payment integrations, authorisation deserves an independent look before a customer, competitor or opportunistic attacker finds the gap. HollowByte reviews the live application and repository together, then provides severity-rated findings and exact remediation work within 48 hours or less.
Your customers do not care whether an access-control failure came from a rushed feature, a generated code block or a misunderstood RLS policy. They care that their data and money stayed within the boundaries your product promised. Make those boundaries enforceable at every request.
Back to blog
