Hunting Authorization Bugs at Scale: Map the Model Before You Attack It

Ask a good pentester which class of vulnerability does the most damage, and the answer comes back with unsettling regularity: authorization bugs. Broken object-level authorization (BOLA), broken function-level authorization (BFLA), and the state-bypass cousins where a banned or unverified user can still act. These are the flaws that let one customer read another customer's records, let a moderator ban an admin, let a low-privilege account reach an endpoint it was never meant to touch. They top the OWASP API Security list for a reason, and they are exactly the bugs automated scanners are worst at, because there is no vulnerable line of code to match on. The flaw lives in a missing check, and a missing check is invisible to anything that only reads what is there.

What has changed is who can hunt them. A capable open-weight model like GLM-5.2 now scores 39% F1 on IDOR and authorization-bug detection — ahead of what flagship coding agents post — at roughly seventeen cents per vulnerability found. That price and that capability put serious authorization hunting within reach of a single attacker with a laptop. The work that used to need a room full of engineers now needs a cheap model and some patience. Attackers are scaling their capability without scaling their headcount, and they are pointing it straight at the bug class scanners can't see.

That is the fight Nullify is built for. Our thesis is that the security work itself — finding real vulnerabilities, triaging them correctly, driving them to a fix — should be automated end to end, so that a defender's outcomes stop being a function of how many engineers they can afford to hire. But "point an LLM at the API" is not that. Turned loose, a model hunting authorization bugs runs up an unbounded bill, buries you in noise, and hands you a verdict with no way to check its work — three things no security program can run on. This post is about our authorization agent, Auth Matrix — our authorization-vulnerability hunter, built for APIs with thousands of endpoints — and specifically about how we keep its autonomy controlled and measurable: bounded in cost, exhaustive within a scope you can name, and auditable at every step. It all follows from one move. Map the API's authorization model before you attack it.

Why brute-forcing endpoints falls apart

The obvious way to hunt these bugs is brute force: take every endpoint, replay it with every credential you were handed, and flag whatever comes back with a 200 that shouldn't. It works, technically. It also does not scale, and why it does not scale is the whole subject of this post.

Consider a real API with a thousand endpoints and a handful of credentials at different privilege levels. If you want an LLM to reason about each endpoint — is this a BOLA candidate, which users should be tested against it, what would count as proof — you are looking at roughly a thousand model calls before you have sent a single test request. Our own early design notes put the arithmetic bluntly: a thousand endpoints at ten cents a call is a hundred dollars per scan just to decide what to test, and that is before the exploitation traffic, before retries, before validation. We have watched a naive overnight run against an app with many credentials and many candidate bugs blow past seventy-five dollars and keep climbing, most of it spent re-deriving context the system already had and testing combinations that were never plausible in the first place.

Cost is only the first problem. The deeper one is that brute force has no model of the application. It cannot tell that /api/v1/posts/{postId} and /api/v1/posts/{postId}/comments operate on related resources, so it never forms the hypothesis that a comment's authorization might inherit — or fail to inherit — a post's. It treats a thousand independent endpoints as a thousand independent problems, when the authorization model that governs them is a small, structured thing: a handful of resources, a privilege hierarchy, and a set of relationships between them. Map that model first, and most of the thousand endpoints collapse into a few dozen questions worth asking. That is the approach Auth Matrix takes, and it inverts the fuzz-everything-and-filter-later reflex.

Build the user model, then cluster the resources

Before Auth Matrix touches the network, it works out who is acting. The credentials and personas the customer supplies at setup describe the privilege levels that matter — admin over member over guest — and the user states that authorization so often turns on: banned, unverified, suspended, restricted. That is a deliberate choice: "what a suspended user is allowed to do" is an application-specific fact, not a universal rule, so it comes from the operator who knows the app rather than a hardcoded assumption. This user model becomes the ground truth everything downstream reasons against; without it, a 200 is just a 200, and there is no boundary to say it crossed.

Only once it knows who the users are does the agent look at what they can reach. It reads the schema — an OpenAPI document, a GraphQL schema, whatever the protocol exposes — and groups endpoints into logical business resources. Given the five endpoints above, it produces two clusters: a posts resource and a comments resource, with comments marked nested under posts. It cleans up the messiness real APIs carry: usr and user collapse into one resource, version segments like v1 and api are ignored, and multi-tenant path shapes like /orgs/{orgId}/projects get flagged as tenant-scoped, so the tenancy boundary becomes something we can test rather than something we miss.

While it reads each endpoint's schema it is also hunting for foreign-key fields — a user_id, a creator_id, an org_id — and recording them as explicit relationships: order.user_id -> user.id. Those relationships are the skeleton of the authorization model. They are what tell the system, later, that the orders cluster needs a hypothesis about one user reaching another user's orders, without anyone hand-writing that rule.

Clustering runs on a fast, cheap model, and it is built to scale. When an API is large enough to matter, the endpoint list is chunked and grouped concurrently, then reconciled — and the reconciliation is a small piece of engineering we are fond of. Rather than re-run the whole analysis to catch the case where chunk A called something user and chunk B called the same thing account, we make one extra cheap call that looks at cluster names and sample endpoints and reports which ones are duplicates. A single call instead of a full re-run. That instinct — spend a little to avoid spending a lot — runs through the entire pipeline.

Generate hypotheses per cluster, still without touching the network

Once the resources are clustered, each cluster gets its own analyzer, and multiple analyzers run in parallel. This is the stage that earns a stronger model, because the reasoning here matters most: given this resource, its endpoints, the users we hold credentials for, and the resources it relates to, what could be broken? The analyzers generate hypotheses and still make no API calls. It is pure schema reasoning — every hypothesis formed before a single test request goes out.

Each analyzer works through a taxonomy organized around four questions. The first is who is allowed to reach an endpoint at all: the classic BOLA and BFLA cases, state bypass where a suspended user can still act, and endpoints missing authentication entirely. The second is what comes back in the response: over-exposure of data, and mass assignment, where a client sets a server-owned field like role or owner_id. The third is when and how a request is permitted: a skipped workflow step, a race condition, a token replayed after it should have been spent. The fourth is what values the server takes on trust: the endpoint that accepts a client-supplied price or total it should have computed itself.

The systematic part is the instruction to enumerate the full cross-product within the cluster: test each role against other users' resources for BOLA; test every write endpoint against a restricted user for state bypass. The efficiency comes from the boundary — only within the cluster. The scope is bounded, so the token cost is bounded, but inside that scope the coverage is exhaustive rather than a lucky guess at one or two endpoints.

So how does a cluster-scoped analyzer reason about cross-resource bugs when it only sees one cluster? Context injection. Each analyzer receives a compact map of the whole application — every resource name, so it knows the full app exists — plus full detail for its own cluster and for the clusters directly related to it through those foreign-key relationships. The orders analyzer sees orders in depth, sees that orders reference users, and pulls in the users cluster, but it is never handed the entire schema of a thousand-endpoint app. That is deliberate token economy: the reasoning stays sharp because the context is relevant, and the bill stays small because we are not re-sending the world to every sub-agent.

Every hypothesis it emits is a structured, inspectable object: a description, the endpoint, the vulnerability class, a business-impact severity, the specific users to test, and the analyzer's own reasoning for why it is worth testing. None of it is a black box. Before any traffic is generated, a human can open the merged hypothesis file and read every hypothesis the system intends to test, cluster by cluster, with the rationale attached.

Only then do you touch the target

Merged and sorted by severity, the hypotheses go to the exploitation stage — the one place real HTTP requests happen. The tester runs hypotheses in batches and carries the analyzer's private reasoning forward, so it does not re-derive context it was already given. Two choices here matter for the efficiency and precision story.

The orchestrator is an LLM loop rather than a hardcoded rule engine, specifically so it can tell an authorization denial apart from an authentication failure. A 403 is usually the application working correctly. A wave of 401s with "session expired" in the body is a dead credential, and the right response is to refresh it and retry the affected tests — not to record dozens of false negatives, and not to keep hammering a target that has gone down. If the whole target is unreachable, the loop escalates and the run aborts cleanly rather than burning budget on hundreds of doomed tests.

The second choice is the evidentiary bar. Before any finding is allowed to be reported as real, the tester has to supply differential proof: it identified a resource belonging to a victim, the attacker credential accessed or modified it, and the response contained data the attacker should not have. If it cannot establish all three, the instruction is explicit — report it as failed. When in doubt, fail it, because false positives waste the customer's time. That discipline is why this approach produces a short list worth reading instead of a long list worth ignoring.

A last, expensive look — where it is cheap to be careful

The successful exploits, and only the successful exploits, go to a final validation pass on the highest-reasoning model with the largest thinking budget in the pipeline. It examines one finding at a time, deliberately, so it gives each its full attention and cannot skim. It applies a structured rubric that separates real differential access from the things that merely resemble it: a 200 with "access denied" in the body, a soft 404, an attacker reaching their own resource, an endpoint that is public by design, a rate limiter or WAF doing its job.

Spending the most expensive reasoning here is cheap in aggregate, because by now the volume is tiny — many hypotheses, few successful exploits, fewer still that survive validation. The cheap models did the large-volume work; the expensive model is reserved for the handful of things that actually reached the finish line. Every confirmed finding leaves behind a reasoning trail for why it was not filtered — another inspectable field, not an opaque verdict.

The same model, whatever the protocol

Because everything upstream reads a schema rather than assuming HTTP verbs and paths, the methodology is protocol-agnostic. We have run the identical pipeline against the same application exposed two ways — as a REST API and as a GraphQL endpoint — and it finds the same class of bug in both. A banned user who can still create posts is the same authorization failure whether the mutation is a POST to a REST path or a createPost GraphQL mutation that authenticates the caller but never checks the isBanned field. The clustering step simply asks the protocol handler how to group its endpoints, and the rest of the pipeline does not care what shape the API takes.

Efficiency, control, and a record of everything

The reason to build it this way is not architectural elegance for its own sake. A security program is judged on outcomes — real vulnerabilities found, triaged correctly, and driven to a fix — measured against the human hours and the token spend it took to get there. Blind fuzzing inflates the denominator and pollutes the numerator with noise. Mapping the authorization model first shrinks the work to the questions that are actually plausible, and the differential-proof and validation gates keep the output to findings a security engineer will trust. We have watched this surface real, high-severity BOLA and authorization-bypass findings at production scale — the kind that need a human's eyes precisely because they are real.

And none of it locks the operator out. Every phase checkpoints its work to disk: the clusters, every per-cluster hypothesis set the moment that cluster finishes, every exploit result. A run that is interrupted resumes from where it stopped instead of re-spending tokens on completed work, and an analyst can open any of those files and read exactly what the system was thinking. The whole run streams live — grouping N endpoints into clusters, analyzing M clusters, testing K hypotheses against the target — so the customer watches the reasoning unfold rather than waiting for a report to drop. Operators who want a heavier hand can route every request the agent makes through a proxy and watch the traffic themselves. The budget is bounded where it actually spends. The exploitation stage — the one place real traffic and the bulk of the cost accrue — runs each hypothesis under a per-hypothesis cost cap and stops itself when it hits that cap, reporting the hypothesis as failed rather than running up an unbounded bill. Upstream, where no network calls happen, the ceiling is the scope itself: the context each sub-agent sees is deliberately kept small, so the token cost of clustering and hypothesis generation is bounded by how much of the app it is handed rather than by a per-call dollar figure.

That is the shape of finding done right, and it is what keeps the defender ahead of an attacker who now has cheap capability too: default to autonomous efficiency, spend the expensive reasoning only where it pays off, prove every finding before you report it, and leave a complete, auditable trail for the human who decides what happens next. The authorization model was always a small, knowable thing. The trick is to map it before you attack it — not to attack everything and hope the map falls out.

Book a demo