Episode 6 · 5 min

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

TellSignalNeeds code?
Scanmany distinct ids with a high miss ratio (mostly 404)no — traffic only
Dumpmany distinct 200 readsno — traffic only
Walka dense, consecutive id rangeno — traffic only
Breacha 200 on an object the caller doesn't ownone 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.