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.
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
the spec is the map
Replay
Verdict
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.
/users/{u}/orders/{o}, where each id needs its own check.