OWASP Top 10:2025 — A01 Broken Access Control for AI-Generated Code

Written by the Rafter Team

You asked the model for "a page where users can see their invoices." It shipped GET /api/invoices/:id, fetched the row by ID, returned it. Looks great in the preview. The bug: nobody checks that the invoice belongs to the logged-in user. Change the number in the URL and you're reading someone else's billing data. This is A01 — and it's the single most common bug we find in AI-generated apps, because the LLM optimized for "make the route work" and there was nothing in the prompt that said "and also enforce ownership."
What A01 actually is
Broken Access Control means a logged-in user can do things they shouldn't — read another tenant's data, hit an admin endpoint they're not an admin of, edit a record they don't own. The classic shape is IDOR: an endpoint takes an ID, fetches the record, returns it, and never asks "does the caller actually own this thing?" Path traversal (../../etc/passwd) and force-browsing to /admin are the same family. Authentication is who you are. Access control is what you're allowed to touch. LLMs are great at the first and quietly skip the second.
Why AI-generated code trips on it
Three patterns we see constantly:
- The lookup-by-id reflex. Asked for "an endpoint that returns the user's profile," the model writes
db.user.findUnique({ where: { id: req.params.id } }). It read the ID from the URL, not the session. If you wanted ownership enforcement, you had to say so. - Frontend-only gating. The dashboard hides the "delete project" button if
user.role !== 'admin'. The backend route doesn't re-check.curl -X DELETEships your data. - Supabase / Firebase without RLS. The model wires up a Postgres table and a client-side query and calls it done. Without Row Level Security policies, anon-key clients can read the whole table. Supabase specifically makes this worse: it works with RLS off, and the warning is easy to dismiss.
- Multi-tenant amnesia. Once you add
organization_id, every query needs to filter on it. LLMs forget on the third refactor. Suddenly Org A is reading Org B's tickets.
The throughline: the model writes code that satisfies the happy path. Access control is a negative requirement — "and nobody else can do this" — and negative requirements don't show up in prompts unless you put them there.
The fix on agentic CLIs (Claude Code, Codex)
Don't ask the agent to "add auth." Ask it to audit, then fix. Paste this:
Audit every route handler in this repo for A01 Broken Access Control.
For each endpoint that reads or mutates a resource:
1. Confirm the user identity comes from the session/JWT, not from a
request param or body field.
2. Confirm the handler verifies the authenticated user owns (or has a
role permitting access to) the specific resource being touched.
3. For Supabase/Prisma/Drizzle: confirm RLS or an equivalent WHERE
clause on tenant_id / user_id is present.
List every endpoint that fails one of these checks. Do not fix yet.
Then have it patch each one and re-run the audit. Two passes catches the regressions the first pass introduces.
If you use Rafter, the Copy-for-AI button on each finding produces a fix-ready prompt scoped to that exact file, line, and CWE — you paste it into Claude Code and it applies the diff. Run rafter run against the diff after, not before, you push.
One non-negotiable: read the diff. Every line. If the model adds if (req.user.id === resource.userId) but the resource was fetched before that check from a different user's row, the check is theater.
The fix on opinionated platforms (base44, Greta, OpenClaw)
You can't always edit the route handler. You can still drive the fix. Two levers:
1. Tell the platform's agent what the rule is, in business terms. Platform agents respond well to ownership language they can translate into their own primitives (RLS policies, permission rules, scoped queries):
"For every table in this app, a user should only be able to read or write rows where they are the owner, or where they belong to the same organization as the row. Apply this at the database level, not just in the UI. Specifically check: invoices, projects, messages, and any table with a user_id or org_id column. Show me the policy you applied for each one."
2. Test it manually before you trust it. Open the app in two browsers, log in as two different users, and try to access User A's resources while logged in as User B. Change IDs in URLs. Hit endpoints from the network tab with the wrong session. If it works, the platform didn't actually enforce the rule, regardless of what it told you. Take that evidence back to the agent.
The constraint on these platforms isn't "you can't fix it" — it's "you can't see whether it's fixed without testing." Build the habit of cross-account testing before launch. A 5-minute manual probe catches what the platform's optimism hid.
See also
- A05 Injection — the other "the LLM wrote the happy path and forgot the adversary" failure.
- A06 Insecure Design — when the access-control model itself is wrong, not just missing.
- A07 Authentication Failures — A01's upstream sibling; broken auth makes broken access control irrelevant.