gethacked.ai

LEARN

IDOR and BOLA, explained

You change a number in a URL and somebody else's data comes back. That is IDOR, also called BOLA. Here is what it is, where it hides, and how to test for it in an afternoon.

Get Hacked AB

In short.

IDOR and BOLA are two names for the same failure: you change a number in a request and the app hands back somebody else's data. The app checks what you may do, and separately which records to return. When the second check is missing, you ask for record 1043 and get it.

The two names

Insecure Direct Object Reference is the older term. It describes the situation: a request names an object directly, and the server returns it without checking who asked. Broken Object Level Authorization is the term OWASP uses in its API Security Top 10, where it sits at number one.

Same failure, two names. When you see either one in a report it means the same class of bug: the object named in the request was never matched against the caller.

What it looks like

An endpoint that takes an identifier. The request looks like GET /api/invoices/1042. The response looks like a normal invoice, with a different customer's name on it. Change the number to 1043 and you get the next one. Nothing errors. Nothing looks unusual in the log.

The part that makes it easy to miss is that every individual request is fine. The status code is 200. The response is well formed. The application is working exactly as it was written.

Why it keeps happening

  • The check lives in the interface. The web app shows the user their own records. The API returns whatever it finds.
  • The query filters by the wrong thing. It filters by account id rather than tenant id, so two workspaces inside one account share everything.
  • The bulk endpoint was never checked. The single-record path got a permission check. The list, search and export paths did not.
  • It was always meant to be internal. Built for a mobile app or an internal service, exposed, and never given a check because it was only for the app.
  • The ids are guessable. Sequential integers. If ids are sequential, testing is a matter of counting.
  • The ids are signed but not scoped. A signature proves the link was generated by you. It does not prove it belongs to the person holding it.

The three checks that stop it

  1. Does this object belong to the caller? Match the object against the caller's tenant, not against the caller's own id.
  2. Does this caller have this action on their role? Reading and deleting are different permissions, and so are reading and refunding.
  3. Did the request arrive the way you intended? Some endpoints carry a signed token that proves the object was deliberately handed to that caller in the first place.

How to test it in an afternoon

You need two accounts, and the second one is the point.

  1. Create a user in tenant A and a user in tenant B. Both are yours.
  2. Sign in as A. Call every endpoint you can find and write down the object identifiers that come back.
  3. Sign in as B. Call the same endpoints with A's identifiers.
  4. Anything that returns A's data to B is a confirmed finding. Record the request, the response and the exact identifier that broke it.
  5. Then do it inside one tenant. Create two workspaces under the same account and check whether workspace one can see workspace two. A lot of products fail here and pass the tenant test.

How to fix it

One rule, applied everywhere: fetch by tenant and id together.

Never look the object up by id and compare afterwards. If you load the record first and check second, the check is one forgotten line away from being skipped, and it runs after the data is already in memory. Put the ownership in the query, so the wrong record is never loaded in the first place.

Then make the negative case a test. The bug that comes back is almost always the one that never had one.