Authentication asks who you are. Authorization asks what you are allowed to touch. An application can get the first one exactly right and still hand over every record it holds, because a valid login is not the same thing as a valid claim to the data behind the next identifier.
In January 2026, a security researcher working under the handle BobDaHacker reported that the official prayer app of the Pope's Worldwide Prayer Network would return any account's details to anyone who asked for them by number. The endpoint was GET /user/users/{id} on the Click to Pray API. In the researcher's published write-up, the description is one sentence long: it "returns user data for any account when you supply a valid user ID. No authorization check."
The identifiers were sequential integers, so the set was walkable from one end to the other. The researcher counted 719,517 registered accounts. Each record carried an email address, first and last name, country, date of birth, and the account's role. One request per user.
Here is the part worth sitting with: the login worked. Sign-in was fine. Sessions were fine. Nothing about the way the application identified its users was broken. What was missing was the second question, the one that has to be asked again on every single request, and it is the question far more programs assume is handled than actually test.
Key takeaways
- Authentication is one gate at the edge of the app. Authorization is a decision repeated on every request, for every object that request names.
- Broken object level authorization is API1 on the OWASP API Security Top 10 because it is both the most common API flaw and the one that scales worst: one missing check exposes every record of that type.
- It hides from generic testing because it has no universal signature. A successful attack and ordinary use produce identical-looking traffic; only your policy says which is which.
- Unguessable IDs raise the cost of finding a target. They are not the control. The entitlement check is the control.
- The technical flaw was one missing check. The six-month exposure was an organizational failure: nowhere to send the report.
Two questions, and only one of them gets tested
Every request to a protected resource asks two separate questions. The first is who is calling. It is answered once, at the boundary, by a login form or a token or a session cookie, and it produces an answer the rest of the system carries around. Teams test it, because when authentication breaks it breaks loudly: nobody can log in, or everybody can.
The second question is may this caller do this thing to this specific object. It cannot be answered at the boundary, because it depends on which object the request names, and that changes every time. It has to be re-answered inside the handler, against the record actually being fetched. That is a lot of places to be right, and being wrong in any one of them is silent.
This is why the two failure modes look nothing alike in production. A broken authentication system generates support tickets on day one. A broken authorization check generates perfectly normal traffic for six months.
What broken object level authorization actually is
The pattern has an older name, IDOR, for insecure direct object reference, and a current one. API1:2023, Broken Object Level Authorization, sits at the top of the OWASP API Security Top 10. OWASP's description of the mechanism is deliberately plain: attackers exploit these endpoints "by manipulating the ID of an object that is sent within the request."
The shape is always the same. An endpoint accepts an identifier from the caller. It uses that identifier to look up a record. It returns the record. Somewhere in that sequence, nobody asked whether the caller was entitled to it, so the application answers a question it should have refused.
What makes it the number one API risk is not sophistication. It is leverage. Most vulnerability classes give an attacker one thing: this page reflects script, that parameter reaches the database. A missing object-level check gives an attacker every record of that type, retrievable in a loop, at the speed of a for-loop. There is no exploit to develop. The feature already works; it just works for the wrong person.
The same application had one of each
The write-up documents a second flaw, and setting the two beside each other is the clearest illustration of the distinction you are likely to find, because they came out of one codebase.
The signup endpoint, POST /user/users/sign-up, returned the new account's validation_hash in its response body. That value was the same UUID used in the email confirmation link. So anyone could register an address they did not control and confirm it immediately, without ever opening the inbox, because the API handed back the token whose only job was to prove they could reach it.
That is an authentication failure. It breaks the application's ability to establish that you are who you claim to be. The user lookup endpoint was the other kind entirely: identity was established correctly, the session was real, and the application simply never asked whether that identity was entitled to the record it was about to hand over.
Two bugs a summary would file together under "the API was insecure." They fail differently, they are found differently, and only one of them can even be described without knowing what the product is supposed to allow.
Why it hides from testing
Most vulnerability classes have a universal signature. SQL injection looks like SQL injection whether the application sells shoes or schedules surgeries: a payload goes in, a database error or a timing shift comes out, and the tell is in the response. Any tool can learn that pattern once and recognize it everywhere, because the pattern belongs to the technology, not to the app.
Object-level authorization has no such tell. Whether user 4,412 should be able to read user 8,819's date of birth is not a fact about HTTP, or about REST, or about the framework. It is a fact about your business. The two requests are byte-for-byte identical except for a number, both return 200 OK, and both return a well-formed record. Nothing in the response distinguishes the legitimate call from the breach. The only thing that can tell them apart is a written statement of who is supposed to see what, and that statement lives in your product requirements, not in any scanner's rule set.
That is also why OWASP's prevention guidance for API1:2023 ends where it does. After advising a proper authorization mechanism and unpredictable record identifiers, the final recommendation is not a tool. It is this: "Write tests to evaluate the vulnerability of the authorization mechanism. Do not deploy changes that make the tests fail."
That is an unusual thing to find at the top of an industry risk list, and it is the honest answer. Authorization is the one class where the specification is yours, so the test has to be yours too.
Testing authorization on purpose
Treat authorization as a property you assert, not a behavior you hope for.
- Write the policy down first. You cannot test a rule nobody has stated. For each resource type, name the roles and the relationships that grant read, write, and delete. This is usually a small table, and building it routinely surfaces disagreements between two engineers who both thought the rule was obvious.
- Make the negative case a test. For every object-scoped endpoint, authenticate as user A, request user B's object, and assert a denial. The positive path already has coverage because it is the feature. The negative path is the security control, and it is the one that silently regresses. Run it in CI so a refactor that drops a check fails the build rather than the customer.
- Check every verb, not just
GET. Read access gets the attention because leaks make headlines, but the same missing check onPATCHorDELETElets someone edit or destroy another tenant's record. Enumerate the methods each endpoint accepts and cover each one. - Use unguessable IDs, and do not count them as the fix. OWASP recommends random GUIDs, and they help, because they take enumeration off the table. They are not a control. Identifiers leak through share links, referrer headers, logs, support tickets, and exports, and an endpoint that never checks entitlement will still honor a leaked one. Opaque IDs raise the cost of finding a target; they do not change what happens when someone does.
- Know the endpoints exist at all. Every item above assumes the endpoint is on your list. Object-scoped routes that never made it into the API inventory get no policy, no test, and no review, so the first requirement is an inventory derived from the application rather than from documentation someone updated last quarter.
The part a tool can do
Everything above assumes you have the list. That assumption is where a good deal of authorization work quietly fails, because the routes most likely to be missing a check are the ones nobody remembered to write down.
An object-scoped endpoint that never reached the spec gets no policy entry, no negative test, and no review. The rule for it was not written wrong; nobody knew there was a rule to write. These are also the routes a crawler will never reach, because nothing in the interface links to them. They sit on an API host and answer only to a client that already knows the path.
That part is automatable, and it is the part worth buying. NightVision derives the API inventory from the application's source, so the routes the code actually defines land on the list whether or not the documentation mentions them, and dynamic testing runs against that inventory fully authenticated.
What it will not do is tell you who should be allowed to reach them. Discovery can hand you an honest list of the doors. Which key opens which one is a statement only your team can make, and the negative tests that enforce it are yours to write. That is the real division of labor: tooling makes sure the inventory is complete, and you make sure the policy is right.
The other bug, the one that lasted six months
The missing check was a few lines of code. The six months of exposure were something else.
The researcher's timeline is specific. On January 3, 2026, they emailed nine contacts: a general information address, six people at the app's own domain, and two at the Prayer Network's. Nobody replied. There was no security.txt, no vulnerability disclosure program, and no bug bounty, so there was no route that led anywhere. The flaw was fixed in July 2026, after a journalist contacted the organization's press address while reporting the story. The fix itself was straightforward once someone acted on it: the endpoint now returns only public profile fields and validates entitlement.
Read that sequence again, because it is the reusable lesson. The vulnerability was found in week one. The fix took a day. What consumed six months was the absence of an address. Somebody had already done the hard part for free and could not find anyone to hand it to.
A security.txt file at /.well-known/security.txt is a few lines of text naming where to send a report. A disclosure policy is a page saying what you will do when one arrives, and who owns it. Neither requires a budget, a team, or a tool. Both are the difference between a researcher's finding becoming a patch and a researcher's finding becoming a headline six months later. If a stranger found a flaw in your application this afternoon, the question is not whether they would tell you. It is whether they could work out how.
Related readingPart of our series on modern application security testing. Start with What You Should Know About Application Security Testing, and see why the endpoints your spec forgot never make it onto any policy in the first place.
Frequently asked questions
What is the difference between authentication and authorization?
Authentication establishes who the caller is. Authorization decides what that caller is allowed to do and which specific records they may touch. Authentication is a single gate at the edge of the application; authorization is a decision that has to be made again on every request, for every object the request names.
What is broken object level authorization (BOLA)?
Broken object level authorization, also called IDOR, is a flaw where an endpoint accepts an object identifier from the caller and returns or modifies that object without checking whether the caller is entitled to it. It is API1 on the OWASP API Security Top 10 for 2023. Any authenticated user, or in some cases any anonymous one, can swap the ID and reach records belonging to someone else.
Do random or UUID identifiers prevent IDOR?
No. Unguessable identifiers make enumeration harder, and OWASP recommends them, but they are defense in depth rather than a control. An identifier that leaks through a share link, a referrer header, a log, or an export is still accepted by an endpoint that never checks entitlement. The authorization check is the control; the opaque ID only raises the cost of finding a target.
How do you test for broken object level authorization?
Write the policy down, then assert it in tests. For each object-scoped endpoint, authenticate as one user, request an object belonging to a different user, and assert the request is denied. OWASP's own prevention guidance for API1:2023 says to write tests that evaluate the authorization mechanism and to refuse to deploy changes that make those tests fail.
What does API discovery have to do with authorization testing?
Authorization testing is scoped to an inventory, so an endpoint missing from that inventory gets no policy and no test. Object-scoped routes are often absent from documentation and unreachable by a crawler, because nothing in the interface links to them. Deriving the inventory from source puts those routes on the list; who may reach them is still yours to decide.
The check that has to be yours
Authentication is infrastructure, and you should buy it rather than build it. Authorization is product logic, and nobody outside your team knows what it should say. That is an uncomfortable division of labor, because it means the highest-ranked risk on the OWASP API list is also the one you cannot delegate to a library or a vendor. What you can do is make the rule explicit, assert it in tests that fail the build, and publish an address where someone can reach you when you get it wrong anyway.