Broken access control: how to test your own app and API

Broken access control is when your app lets a signed-in user see or change something that belongs to someone else, or reach a feature meant for a different role. It is the most common serious flaw in web apps, and it is easy to miss, because every page still looks right when you test it as yourself. This guide shows you how to test your own app and API for it with two test accounts, and how to fix what you find.

Why broken access control sits at the top of the list

The OWASP Top 10 is the most widely used list of web app security risks. In its 2025 edition, broken access control keeps the number one spot, and OWASP reports that 100% of the applications tested were found to have some form of it. That does not mean your app is broken. It means the flaw is so ordinary that you should assume it is there until you have checked.

The reason it is so common is simple. Sign-in answers the question "who are you?". Access control answers a different question: "may this person do this, to this record, right now?". Many apps answer the first question carefully and the second one only in the interface, by hiding buttons and menu items. Hiding a button is not a check. Anyone can send the request the button would have sent.

The two shapes it takes in an API

The OWASP API Security list splits the problem in two, and the split is a useful way to organise your testing.

  • Broken object level authorization (BOLA). The endpoint is one the user is allowed to call, but the record it returns belongs to someone else. OWASP puts this first among API risks and calls it extremely common, because servers often trust the ID the client sends.
  • Broken function level authorization (BFLA). The user should not be able to reach the endpoint at all, such as an admin route that an ordinary account can call.

You will test both, but the first is where most apps leak data, so start there.

A worked example: the invoice that was not yours

Say you run a small billing app. A signed-in customer opens an invoice, and the page loads it with a request like GET /api/invoices/1043. The server looks up invoice 1043 and returns it. The page only ever shows customers their own invoice numbers, so in normal use nothing goes wrong.

Now repeat that request yourself, still signed in as the same test customer, but ask for /api/invoices/1044, an invoice you created earlier under a second test account. If the server returns it, you have found broken object level authorization. The server checked that the request came from a signed-in user. It never checked that this user owns invoice 1044.

The fix is one rule, applied on the server: load the record, compare its owner with the user from the session, and refuse when they differ. In practice that often means scoping the lookup itself, such as where id = :id and account_id = :session_account instead of where id = :id. Because the check now lives in the query, forgetting it in one route becomes much harder.

Two details from OWASP are worth keeping in mind. First, comparing the user ID in the session with a user ID in the URL is not enough on its own; what matters is whether the user may act on the record being requested. Second, random, hard-to-guess IDs make guessing harder, but they are a second layer, not the fix. A shared link or a log file can still hand someone a valid ID.

How to test your own app for broken access control

You do not need special tools to start. You need two ordinary test accounts, an admin test account if your app has roles, and your browser's developer tools to see the requests the app makes. Test in a development or staging copy, with test data, and only against systems you own or are allowed to test.

  1. Map the records. List every endpoint that takes an ID: in the path, the query string, the request body or a header. Orders, invoices, files, messages, profiles and team settings are the usual places.
  2. Create the same kind of record in both accounts. Note the IDs that belong to user A and to user B.
  3. Swap the IDs. Signed in as user A, repeat each request with user B's ID. Try reading, then updating, then deleting. A correct app refuses and changes nothing.
  4. Try the routes your role should not reach. Signed in as an ordinary user, call the admin endpoints you noted while signed in as the admin test account. Try other methods too: a route that refuses GET may still accept DELETE.
  5. Test signed out. Repeat the key requests with no session at all. Everything that is not meant to be public should refuse.
  6. Read what comes back, not only the status. A response can succeed and still carry another user's email address in a nested field.

The pre-launch checklist for sign-in and payments covers the sign-in side. The steps above cover what happens after someone is signed in.

Fixes that hold up over time

Finding one leaky route is useful. Making the whole class of mistake rare is better. These come from the OWASP Top 10 and the OWASP Authorization Cheat Sheet:

  • Deny by default. Apart from public pages, refuse a request unless a rule allows it. A new route that nobody wrote a rule for should fail closed.
  • Check on the server, on every request. Checks belong in trusted server-side code, not in the interface. As the cheat sheet puts it, an attacker only needs to find one way in, so checking most requests is not enough.
  • Put the check in one place. A shared policy layer or middleware is easier to get right than a check copied into each handler.
  • Enforce record ownership. Scope queries to the signed-in user or their team, as in the worked example.
  • Give each role the least it needs. It is easier to grant a permission later than to take one away.
  • Log refusals and watch them. A burst of access control failures from one account is worth an alert.
  • End sessions properly. Invalidate sessions on logout, and keep tokens such as JWTs short-lived.

Turn your manual checks into tests

The manual pass finds today's gaps. Automated tests stop them coming back. For each endpoint that takes an ID, write a test that signs in as user A, asks for user B's record, and expects a refusal. Do the same for each role boundary. OWASP's API guidance says to treat these tests as a gate: do not deploy a change that makes them fail.

Keep your expectations honest. The Authorization Cheat Sheet notes that unit and integration tests catch the low-hanging fruit well but do not replace a dedicated security test or manual testing. Business rules such as "a team member may view a payment but not refund it" are where automated checks are weakest, because a tool does not know your rules.

When to bring in an outside tester

Run the steps above yourself first; they cost little and catch a lot. An outside test is worth it when your app holds other people's money or private data, when you have several roles and teams, or when a customer asks how you tested it. Someone who did not build the system will try paths the builders took for granted.

That is part of what whitehatstoic does: find and fix weak spots before they cause harm. Its cybersecurity and AI safety testing covers web app and API review, ends with a written report and fixes, and includes a retest after the fixes. Testing is scoped after a short call. No test can promise to find every weakness, so the aim is to find the ones that matter and help you close them.

Frequently asked questions

Is broken access control the same as IDOR?

Insecure direct object reference (IDOR) is one form of it: the app uses an ID from the request to fetch a record without checking who owns it. Broken access control is the wider family, which also covers reaching functions your role should not reach.

Do random IDs fix broken object level authorization?

No. Hard-to-guess IDs make guessing harder, but the server must still check that the signed-in user may act on the record.

Should I run these tests in production?

Run them in a development or staging copy with test accounts and test data. If you need to confirm something in production, use only accounts you own and agree it with whoever runs the system first.

How often should I repeat the checks?

Run the automated tests on every change. Repeat the manual pass whenever you add a new role, a new kind of record, or a new way to share data between users.

Get started

Building something that has to be safe? Book a meeting with whitehatstoic: tell us the product, the deadline and your biggest worry, and we reply with a plan and a price.

0 likes

Comments

No comments yet.

Sign in or make an account to comment.