This month, a researcher showed that the Vatican’s Click To Pray app exposed the names, email addresses, countries and dates of birth of all 719,517 registered users. Each account had a sequential numeric ID, and the API returned any account’s data to anyone who asked for it by number. The researcher reported it in January and said there was no response for six months.

That bug has a name: broken object-level authorization, or BOLA. It’s the first entry in the OWASP API Security Top 10, and it’s the flaw we most expect to find when we test an API.

The one request

Here it is. A user signed in as account A asks for an order:

GET /api/v1/orders/18342 HTTP/1.1
Host: api.example.com
Authorization: Bearer <token for account A>

Order 18342 belongs to account B. The API checks that the token is valid, finds the order and returns it. Authentication worked. Authorization never happened. Change the number and you can read every order in the system.

Why scanners miss it

To a scanner, that response looks perfect: a 200 status, valid JSON and no error. Nothing in it says “this belongs to someone else.” Finding BOLA requires knowing who owns what, which means testing with at least two accounts and comparing what each can see. That is why it survives automated scanning and turns up in manual testing.

It’s also why it’s so common. Every endpoint that takes an ID needs its own check, and it only takes one forgotten endpoint. In 2025, researchers Ian Carroll and Sam Curry found that McHire, the hiring platform used by McDonald’s and built by Paradox.ai, exposed job applications from more than 64 million applicants through an admin account protected by the password 123456 and an API that returned any applicant’s records when the ID was changed.

Random IDs don’t fix it

Switching to UUIDs makes IDs harder to guess, and that helps against mass enumeration. It doesn’t fix the bug. IDs leak through URLs, logs, shared links and other API responses. If the server would return the object to the wrong user, the bug is still there.

The pattern that prevents it

Make ownership part of the query, not an afterthought:

// Vulnerable: fetch by ID, then return
const order = await db.order.findUnique({ where: { id } });

// Fixed: the lookup is scoped to the caller's account
const order = await db.order.findFirst({
  where: { id, accountId: req.user.accountId },
});
if (!order) return res.status(404).end();

Return 404 rather than 403, so the response doesn’t confirm the object exists. Then make the safe version the easy one:

  • Centralise it. Put tenant or owner scoping in a shared data-access layer or policy engine, so individual handlers can’t forget it.
  • Deny by default. New endpoints should fail closed until an authorization rule exists.
  • Test it in CI. For every endpoint that takes an ID, add a test where account A requests account B’s object and expects a 404.
  • Review for it. In code review, any lookup by ID without an owner or tenant condition deserves a question.

Check your own API today

Create two test accounts. Sign in as the first, create something, and note its ID. Sign in as the second and request that ID on every endpoint that accepts it, including updates and deletes. If anything comes back, you’ve found the most common serious API bug there is, before someone else did.

Sources

  1. Malwarebytes: Vatican’s Click To Pray app exposed personal data from 700,000 users (Jul 2026) (opens in a new tab)
  2. Cybernews: Vatican prayer app exposed names and emails of 700,000+ users (Jul 2026) (opens in a new tab)
  3. Ian Carroll and Sam Curry: Would you like an IDOR with that? Leaking 64 million McDonald’s job applications (Jul 2025) (opens in a new tab)
  4. OWASP API Security Top 10 2023: API1 Broken Object Level Authorization (opens in a new tab)