Catching IDOR
IDOR is the #1 API risk and the quietest bug there is — a single request looks like a normal 200. You can't catch one request, so you catch the shape of an attacker hunting for it.
What you'll learn
- ✓Why a single IDOR request is undetectable
- ✓Catch scan, walk, and dump from the traffic alone
- ✓Confirm the real breach with one event
- ✓Fix it in one line — scope the read to the owner
Catching IDOR
IDOR — insecure direct object reference — is OWASP's #1 API risk: you change an id in a URL and read data that isn't yours. It is also the quietest bug there is. One IDOR request returns a perfectly normal 200 — no error, nothing to catch. So you can't catch it one request at a time. You catch it by the shape of the traffic when someone hunts for it.
What IDOR hunting looks like
An attacker hunting IDORs does three things:
- Scan — fire the same route at id after id, looking for which objects exist.
- Walk — 1, 2, 3 … straight up the range, because integer keys are predictable.
- Dump — once they find data, pull every object they can reach.
Every one of those requests, on its own, is a valid authenticated 200 or an ordinary 404. No single request is an attack — but the distribution gives it away. That is statistical detection.
The four tells
| Tell | Signal | Needs code? |
|---|---|---|
| Scan | many distinct ids with a high miss ratio (mostly 404) | no — traffic only |
| Dump | many distinct 200 reads | no — traffic only |
| Walk | a dense, consecutive id range | no — traffic only |
| Breach | a 200 on an object the caller doesn't own | one event |
Step 1 — The scan detector, in SQL
Three of the four need no app code — they read the traffic SecureNow already captures. The scan detector is one query:
SELECT ip, route,
uniqExact(id) AS distinct_ids,
countIf(status = '404') AS misses,
round(misses / count(), 2) AS miss_ratio
FROM traces
WHERE route LIKE '/api/notes/:id'
GROUP BY ip, route
HAVING distinct_ids >= 20 AND miss_ratio >= 0.4 -- a scan
Step 2 — Build the detections (test mode)
Create the four rules, each validated against real traffic, each detect-only:
securenow alerts rules create --name "IDOR: id-space scan (high miss ratio)" \
--sql @rules/idor-scan.sql --apps <APP_KEY> --severity high --schedule "*/15 * * * *"
# …and idor-dump, idor-sequential-walk, idor-cross-owner
Step 3 — Attack your own lab
Run a scan, a walk, and a dump against a note resource keyed by a plain integer, plus a legitimate baseline:
baseline .60 reads own 6 notes → 6×200 (trips nothing)
scan .61 80 random ids → 6×200, 74×404 (90% miss = scanning)
walk+dump .62 sequential 1..30 → 30×200 (walk + dump of others' notes)
Step 4 — The distribution is the signal
In the dashboard's trace explorer, the status-code summary tells the whole story:
- Scanner (
.61): 90% 404 — probing for what exists. - Dumper (
.62): 100% 200 — pulling it all. - Baseline (
.60): low distinct, zero misses, reads only its own — fires nothing.
No single request looked wrong; the distribution gives each attacker away.
Step 5 — Behaviour vs breach
The traffic rules flag the behaviour — someone is hunting. But a scan could be noise, and a power user could read a lot of their own data. So the fourth rule reads one event and counts the grants where the owner isn't the caller:
// when the app returns an object, emit the grant:
track("authz.granted", { userId, object_id: note.id, owner_id: note.ownerId });
A legitimate read of your own data never fires it. An IDOR does. That is not behaviour — that is the breach, confirmed.
Step 6 — Fix the code
Detections contain it; the fix closes it, in one line — scope the read to the caller:
SELECT * FROM notes
WHERE id = $1 AND owner_id = $2; -- not yours => 404, no existence leak
A note that isn't yours now returns a 404, the same as one that doesn't exist — no data leak, and no hint the object is even there. A test proves it: the owner reads their note, everyone else gets 404.
What you just did
You caught the #1 API bug that a single request can never reveal — from the shape of the traffic — confirmed the real breach with one event, and closed the hole in one line. A single IDOR request will always look innocent; an attacker can't hunt without leaving a shape. Catch the pattern. Confirm the breach. Close the hole.
Try it on your own app
Start the threat model, then plug SecureNow in — the whole series works on your project today.