RTRadu Todea ▚ / Case study ← All write-ups

Case studyauthz-replay

Your key, someone else’s door.

The most common API bug isn’t clever. A route like GET /notes/{id} loads the note by its id and forgets to ask whose it is. So I built a small open-source tool that checks for exactly that, the way a tester would: read things as one user, then ask for the same things as another.

Type
Open-source tool · 2026
Built with
Python · OpenAPI
Finds
BOLA, OWASP API #1
Touches
Reads only

01The bug

An old bug, under a new name.

Broken object-level authorization (BOLA) tops the OWASP API Security Top 10. It is the same flaw web testers have long called IDOR: the server checks that you are signed in, but never checks that the object you asked for is yours. Every user holds a valid key; the question is whether each door checks it belongs to them.

The fix is a check on the server: does this object belong to the caller? Making ids hard to guess, with random UUIDs instead of 1, 2, 3, doesn’t fix it. It only hides the door; it doesn’t lock it.

02How it works

Read as one user, ask as another.

You give it the API’s OpenAPI document and the tokens of two test users. The document is the map, so it needs no rules written for your API. Then it runs in two passes.

Record

As user A Read every list, collect the ids, open each object. Keep what A can see.

the spec is the map

Replay

As user B, and with no token Ask for each of A’s objects again, exactly as A did.

Verdict

Same object back? Only when the reply carries A’s object, the same body or the same id, is it a finding.

a 200 alone is not proof

That last step matters most. User B may well have their own object at the same route, so a successful response on its own proves nothing. A scanner that raises false alarms soon gets ignored, so this one only reports an object that demonstrably crossed from one user to another.

03A run

A test target, caught.

The tool ships with a small, deliberately vulnerable API to practise on: notes that belong to two users, one route that forgets to check the owner, one that needs no token at all, and one that does everything right. This is a real run against it, trimmed:

$ authz-replay --config demo/local-config.yaml
  requests sent   23
  ids harvested   3
  objects probed  8
  findings        6

[1] HIGH  Broken object-level authorization
     GET /notes/1 returned the same object to two different users
     as user A: 200   replayed: 200

[4] HIGH  Object served without authentication
     GET /public/1 returned an object with no token
     as user A: 200   replayed: 200
     … and four more like these

Three notes leaked across users, and three were served to anyone. The route that checks the owner, /safe/{id}, answered B with a 403 and was left alone, which is the other half of the job: no false alarm.

04Built to be safe

Safe to point at staging.

Reads only
The HTTP layer sends GET and nothing else. There is no flag to make it write, so it can’t change data on the target.
An allowlist
Requests go only to the API’s own host and any you add. A URL anywhere else is refused before it is sent.
Rate limited
Requests are spaced out, five a second by default, so a test never floods the thing it is testing.
Permission first
It refuses to start without an explicit statement that you are authorized to test the API.
In the pipeline
It writes SARIF, which GitHub shows in the Security tab, and exits non-zero on a finding, so a deploy can fail on a new BOLA.

05What it doesn’t do yet

A first pass, on purpose.

TodayWhy it mattersNext
One id per route Nested routes are skipped Handle paths like /users/{u}/orders/{o}, where each id needs its own check.
Ids from list endpoints An object never listed is never tried Accept known ids for each user, so coverage doesn’t depend on what the API lists.
Reads only Editing or deleting someone else’s object goes untested Opt-in write checks, only against a disposable test environment.
A clean run Means the reads it probed were enforced Not proof an API is secure. It is a first pass that is cheap enough to run on every deploy.