AI Code Review for Authorization Bugs: Missing Permission Checks, IDOR and Privilege Escalation
Broken access control tops the OWASP list because it hides in what a diff leaves out. Here is how to make a reviewer look for the absence.

When OWASP revised its Top 10 in 2021, Broken Access Control moved to the top of the list. It has stayed there in practice for a simple reason: an authorization bug is not a bad line of code, it is a missing one. A handler that forgets to check that the caller owns the record looks exactly like a handler that never needed to. Static analysers, which hunt for dangerous patterns, have nothing to match on. Tests written by the author of the feature test the happy path, as the author of the feature always does. And a code reviewer reading the diff sees twenty tidy lines and a green CI run.
This post is about pointing an AI code reviewer at that absence. It lists the six shapes authorization bugs take in ordinary web services, what a reviewer needs in order to catch each one, and how to turn that into review instructions. Where PURA does something specific we say so; most of it applies to any reviewer with the repository in front of it. It is a companion to the general security review post and the one on injection vulnerabilities, which cover the bugs that do show up as patterns.
Why the diff is the wrong place to look
Consider a pull request that adds GET /api/invoices/:id. The diff contains a route, a handler that loads the invoice by id, and a serializer. Nothing in it is wrong. The question a reviewer has to answer is whether the handler should also check that the invoice belongs to the caller’s organisation, and the only way to answer it is to look at the handlers next to it: GET /api/invoices filters by organisation, PATCH /api/invoices/:id calls assertCanEdit(invoice, user), and this new one calls neither. The evidence is in the files that did not change.
We wrote in why diff-only reviewers miss real bugs that context is the largest difference between review tools. Authorization is the category where that gap is widest, because the correct behaviour is defined by convention rather than by any single file. A reviewer that cannot open the sibling handlers can only comment on style.
The six shapes
1. The missing check on a new endpoint
The most common case and the simplest to describe: a new route, action or resolver with no authorization at all, or with authentication only. The reviewer should, for every new entry point, identify the project’s authorization mechanism (a decorator, a middleware, a policy class, a helper called at the top of each handler) and confirm the new entry point uses it. If the project has no single mechanism, that is itself the finding, and the review comment should say so rather than inventing one.
2. Insecure direct object reference
An identifier from the request is used to load a record and the result is returned or modified without checking the caller is allowed to touch that record. Sequential integer ids make it trivial to exploit; UUIDs make it harder to guess but no less wrong. The check the reviewer needs is mechanical: trace every parameter that becomes a primary key lookup and confirm an ownership or membership condition is applied before the data leaves the handler. The subtle variant is the nested one, where the parent is checked and the child is not: the caller may view project 42, so the handler returns task 9000, which belongs to project 43.
3. Mass assignment of privileged fields
A request body is bound directly to a model and saved. If the model has fields such as role, is_admin, organisation_id or plan, a client can set them. Frameworks have had allow-lists for this for a decade and developers still reach for the convenient form. The reviewer should flag any write path that binds request data without an explicit list of permitted fields, and should know which fields in the schema are privileged, which means it needs the schema, not the diff.
4. Privilege escalation through roles and scopes
Any change to how roles, permissions, scopes or feature flags are assigned deserves a slower read. The classic bugs: a user can change their own role; an invitation endpoint lets the inviter grant a role higher than their own; a scope check compares strings with a prefix match so read:billing satisfies the narrower read:bill scope, and a token refresh preserves permissions that were revoked. These are logic bugs with no syntactic signature, and the reviewer’s best tool is to state the invariant in plain words (“nobody can grant a role above their own”) and check the new code against it.
5. Tenant leakage in queries
In a multi-tenant system almost every query carries a tenant filter, and the bug is the one that does not. It appears in reporting code, in background jobs, in search, in any place where the query is built by hand rather than through the scoped repository. The reviewer should treat the tenant key as a required predicate and flag any new query against a tenant-owned table that lacks it, including raw SQL and ORM queries that start from the wrong end of a relationship.
6. Enforcement in the wrong layer
A button is hidden in the UI, so the feature is “admin only”. A route is protected by middleware, so a new route added outside the middleware’s path prefix is not. A GraphQL field is guarded on one query type and reachable through another. The reviewer should look for where the enforcement actually lives and ask whether the new code is inside it. Client-side checks are a usability feature, and a review comment that says so once, politely, saves an incident.
Turning the list into review instructions
A reviewer cannot know your authorization model unless you write it down, and the model is rarely written down anywhere a reviewer will find it. The fix is a short, specific set of project instructions. PURA reads these from the repository and applies them to every pull request; the shape works for any tool that accepts custom review rules.
- Where authorization lives: “every HTTP handler calls
authorize()fromlib/authzbefore touching the database; flag any handler that does not.” - The ownership rule: “records with an
organisation_idcolumn may only be read or written when it equals the caller’sorganisation_idvalue, and lookups by id that do not include this predicate are flagged.” - The privileged fields: “
role,organisation_id,planandverified_atmay never be set from request bodies.” - The role invariant: “no code path may assign a role with a higher rank than the caller’s.”
- The enforcement layer: “authorization decisions in the web client are not authorization; any feature gated only in the client should be flagged.”
Five lines. With them, a reviewer that has the repository open can check each new handler against the convention, each new query against the tenant rule, and each new write path against the field list, and it will do so on every pull request including the one opened at 17:55 on a Friday. Without them it is pattern-matching against whatever it has seen in the codebase, which works until the codebase itself is inconsistent.
Advisory or blocking?
Authorization is one of the few categories where a blocking review is defensible. A false positive costs the author a one-line reply explaining why the check is unnecessary; a false negative is a data breach. The argument in blocking vs advisory review still holds, though: block on the specific, mechanical findings (a handler with no authorization call, a query with no tenant predicate, a privileged field in a mass assignment) and keep the judgement calls (is this role change a privilege escalation?) advisory, with a named human reviewer required for anything that touches lib/authz itself.
What the reviewer cannot do
It cannot tell you what the rules should be. If your product has never decided whether a project viewer may export the project, no reviewer will decide it for you, and the review comment will be a question rather than a finding. It cannot see runtime configuration: a permission set in an admin console, a feature flag flipped in production, an identity provider mapping groups to roles. It cannot exercise the endpoint as a second user, which remains the single most reliable authorization test and belongs in your integration suite. And it will, occasionally, insist on a check that the framework already performs somewhere it did not look. Treat the review as the checklist that never gets skipped and the test suite as the proof.
The payoff is that the most common serious vulnerability class stops depending on whether the one reviewer who knows the authorization convention happened to be online. Every pull request that adds an entry point gets the same six questions asked, with the sibling handlers open, before a human reads it. For the bug that hides in what the diff leaves out, that is the only kind of review that works.
Frequently asked questions
- Why are authorization bugs hard for AI code review?
- Because the bug is usually an absence, not a line. A handler that forgets to check ownership looks identical to one that never needed to, and the diff contains no evidence either way. The reviewer has to know the project convention for authorization, find the equivalent check in sibling endpoints, and notice that this one does not have it, which requires repository context rather than the changed lines.
- What authorization issues should an AI reviewer check for?
- Missing or incomplete permission checks on new endpoints; insecure direct object references where an identifier from the request is used without an ownership check; mass assignment of privileged fields; privilege escalation through role or scope changes; cross-tenant leakage in queries that forget the tenant filter; and authorization enforced only in the client or in middleware that the new route bypasses.
- How do you make AI code review catch authorization bugs reliably?
- It cannot know which users are supposed to see what unless the rules are written down, so give it the authorization model as review instructions: where checks live, which helper to use, which fields are privileged, and what the tenant key is. With those rules it can flag deviations consistently; without them it can only guess from patterns in the codebase.
Ready to put your AI review spend on rails?
Install PURA on your GitHub repos and start setting budgets in minutes — not months.
Install PURA for free