Broken Access Control Checklist · Find and Fix OWASP A01
Quick answer: Broken access control (OWASP A01) is when your app lets a user do something they should not · read another user's data, edit a record they do not own, or reach an admin action. This checklist groups the fixes by layer · object-level ownership checks, function-level role checks, database RLS, and verified JWT sessions.
By Paula C · Kraftwire Software
· 7 min readWhat Is Broken Access Control?
Broken access control is what happens when your app lets a user do something they should not be allowed to do. They read another person's data, edit a record they do not own, or reach an admin action without being an admin. The rules exist in your head and maybe in your UI, but the server never actually enforces them.
It sits at the top of the OWASP Top 10 as category A01, ahead of injection and cryptographic failures. It earned that spot because it is both common and high-impact: one missing check can expose every user's records at once. For a full tour of where it fits, see the OWASP Top 10 for AI-built apps.
Access control breaks in three distinct places, and it helps to name them:
- Object level · can this user touch this specific record? Failures here are called IDOR (insecure direct object reference).
- Function level · can this user perform this operation at all? Missing function-level access control is how non-admins reach admin actions.
- Data layer · does the database itself enforce ownership, or does it hand back any row that is asked for?
AI code generators are unusually good at producing the first 90% of a feature and quietly skipping the authorization. The UI hides the button, so the demo looks correct, and the missing server-side check never surfaces until someone goes looking.
What Are Real Examples in Vibe-Coded Apps?
These are the patterns that show up over and over in apps built with Lovable, Bolt, Cursor, Replit, and v0. None of them require a skilled attacker · just curiosity and browser dev tools.
IDOR Through Sequential IDs
Your app loads an invoice at /invoice/1043. A user changes the URL to /invoice/1044 and sees someone else's invoice. The endpoint fetched the row by ID and returned it without ever checking who owns it. Sequential integer IDs make this trivial to exploit because the attacker just counts up.
Admin Routes Hidden Only in the UI
The React frontend hides the /admin link unless user.role === 'admin'. But route guards in the browser are cosmetic. An attacker types the URL directly, or calls the underlying API endpoint, and the admin panel loads because the server never re-checked their role. This is the single most common authorization gap in AI-generated code, and we cover it more in architecture security risks.
Supabase Tables With No RLS
When a table ships without Row-Level Security enabled, your public anon key (which lives in every visitor's browser bundle by design) grants unrestricted access to every row. Anyone can query the whole table directly through the Supabase API, no login required. See RLS policies explained and the Supabase security checklist for the fix.
JWT Role Trusted From the Client
The app reads the user's role from a token or a request body and believes it. If the role is set client-side, or the token signature is never verified server-side, a user edits "role":"user" to "role":"admin" and escalates privilege. Decode any token with the JWT debugger and read the JWT security guide to see why the signature is the only part that matters.
The Broken Access Control Checklist
Work through this before launch. Each item is a place authorization tends to go missing. Group by layer so nothing is enforced in only one spot.
Object-Level Access (IDOR)
- Every endpoint that fetches a record by ID also checks that the current user owns or may access that record
- Ownership is verified server-side, never inferred from a hidden form field or the URL
- Resource IDs are UUIDs, not sequential integers that can be enumerated
- List endpoints filter by the authenticated user, so they cannot return other users' rows
- File and storage downloads check ownership before serving the object
Function-Level Access
- Every admin or privileged action re-checks the user's role on the server
- Route guards in the frontend are treated as UX only, never as the security boundary
- API endpoints are protected individually · hiding a button does not protect the endpoint behind it
- Destructive operations (delete, refund, role change) require an explicit permission check
- Default is deny · a new endpoint rejects requests until access is explicitly granted
Data Layer and RLS
- Row-Level Security is enabled on every table that holds user data
- Policies exist separately for SELECT, INSERT, UPDATE, and DELETE
- Policies scope rows with the verified identity (
auth.uid()), never a client-supplied ID
- The service role key lives only server-side and never ships in the browser bundle
- No table is reachable by the anon key beyond what its policies intentionally allow
Session and JWT
- Tokens are verified server-side on every request · signature, expiry, and issuer
- The user's role and permissions come from the server or a verified claim, never client state
- Sessions expire and can be revoked · a logged-out or banned user loses access immediately
- Tokens are not readable or writable by client JavaScript where it can be avoided
- No sensitive authorization decision depends on a value the client can edit
How Do You Test for Broken Access Control?
You do not need a pentest lab. The fastest tests are manual and take minutes. This overlaps with the broader web app security audit checklist.
Create a second account. Log in as User A, note the ID of a record you own, then log in as User B and try to open User A's record by ID. If B can see it, you have an IDOR.
Swap IDs in the URL. Increment or change the ID in any address bar path or query string. Watch for data that is not yours loading successfully.
Hit protected URLs directly. Log out, then paste an authenticated route like /admin or /settings straight into the browser. It should redirect or reject, not render.
Call the API with and without an auth header. The UI is not the app · the API is. Test the endpoint directly:
Then test whether one user can reach another user's object even while authenticated:
For a Supabase table, query it directly with the public anon key. If rows come back without a matching policy, RLS is missing:
If that returns every invoice in the table, anyone can do the same.
How Do You Fix It?
The fixes are consistent regardless of platform, because the failure is always the same: a decision made in the wrong place.
- Deny by default. Start every endpoint and every policy from "no access," then grant the narrow case you intend. An endpoint with no explicit check should reject, not allow.
- Move every check server-side. Frontend guards stay for UX, but the authoritative decision happens where the client cannot reach it · in an API handler, edge function, or database policy.
- Verify ownership on every object. Fetching by ID is not enough. The query must include the current user's identity, so a request for a record you do not own returns nothing.
- Enforce it at the data layer with RLS. Row-Level Security is the backstop that holds even if an application check is forgotten. Scope every policy with
auth.uid(), and split policies per operation.
- Trust only verified identity. Read roles from a server-verified token claim or a database lookup, never from a request body, header, or local storage value the client controls.
The theme is defense in depth. A frontend guard plus a server check plus an RLS policy means a single mistake does not become a breach. The vibe coding security checklist walks through applying this across a whole app.
Can You Detect It Automatically?
Partly. Some access-control flaws are visible from the outside · missing security headers, unauthenticated endpoints that return data, tables reachable with the anon key, and tokens with obvious red flags. Others, like whether User B can read User A's specific record, need context about your data model that no external scanner fully has.
That is why manual second-account testing stays essential. But an automated scan quickly rules out the common, catastrophic cases so you can spend your manual time on the subtle ones.
SimplyScan runs 51+ automated checks across 14 categories in about 30 seconds, no signup. It flags missing or weak Supabase RLS, unprotected endpoints, exposed keys that enable privilege escalation, and the header and auth gaps that make access control easier to break. Scan your deployed app, fix what it finds, and then run the manual ownership tests above to close out OWASP A01.
Frequently asked questions
What is the difference between broken access control and broken authentication?
Authentication is proving who you are, which is logging in. Access control is what you are allowed to do once logged in. Broken authentication lets an attacker become another user, while broken access control lets an already-logged-in user reach data or actions beyond their permission. OWASP tracks them separately, as A01 and A07, and an app can fail one while passing the other.
What is an IDOR vulnerability?
IDOR stands for insecure direct object reference. It happens when an endpoint fetches a record by its ID without checking that the requester owns it. Changing /order/1043 to /order/1044 and seeing someone else's order is a classic IDOR. Sequential integer IDs make it worse because attackers can simply count upward. Using UUIDs helps, but the real fix is a server-side ownership check.
Is broken access control the most common web vulnerability?
It is the top category in the OWASP Top 10, listed as A01, because it appears in a large share of tested applications and often exposes many records at once. In AI-generated apps it is especially common, since code generators build the feature and the UI but frequently skip the server-side authorization check that actually enforces the rule.
How do I test for privilege escalation?
Create a low-privilege account and try to perform actions reserved for admins. Call admin API endpoints directly with your normal user token, paste admin URLs into the browser, and edit any role value you can reach in a token or request body. If the server honors the change instead of rejecting it, you have a privilege escalation flaw. Always test the API, not just the UI.
What is missing function-level access control?
It is when an application controls access to a page in the UI but not to the underlying operation on the server. The admin button is hidden, yet the admin endpoint answers anyone who calls it. The fix is to re-check the user's role on the server for every privileged action, treating frontend route guards as cosmetic rather than as a security boundary.
Can RLS alone prevent broken access control?
Row-Level Security is a strong backstop at the data layer, and every user table should have it. But it does not cover function-level actions handled outside the database, unverified JWT claims, or business logic in edge functions. Use RLS together with server-side checks and verified identity so a single missed control never becomes a full breach. Defense in depth is the goal.