IDOR Hunting at Scale: From Manual Testing to Automated Discovery

IDOR Hunting at Scale: From Manual Testing to Automated Discovery

A systematic methodology for finding broken object-level authorization across every method, endpoint, and hidden reference, not just the obvious ones.

HackerSavanna Security Team

Security research and platform engineering at HackerSavanna.

5 min read3 views

Insecure Direct Object References are the bug class that never runs out of runway. They're conceptually trivial (change an ID, see someone else's data), they show up in nearly every application that has more than one user, and they're the single most common finding across HackerSavanna's public disclosures. This is a practical guide to finding them faster and more thoroughly than "change the number in the URL and hope."

What actually counts as an IDOR

An IDOR happens whenever an application uses a client-supplied identifier to fetch or modify a resource, without independently checking that the requesting user is allowed to touch that specific resource. The identifier doesn't have to be a sequential integer in the URL. It can be:

  • A UUID in a JSON body ({"invoiceId": "..."})
  • A resource key inside a GraphQL query
  • An object reference buried three levels deep in a nested API response
  • A file path or storage key returned by an earlier, legitimate request

The common thread is always the same: authentication proves who you are, but the endpoint forgot to check what you're allowed to touch.

Manual testing, done properly

The naive version of IDOR testing is incrementing an ID and seeing what happens. That catches the easy ones. To catch the ones everyone else missed, test every axis:

  1. Horizontal escalation. Two accounts, same privilege level. Account A tries to read, update, and delete Account B's resources.
  2. Vertical escalation. A low-privilege account (free tier, unverified researcher, guest role) attempts actions that should require a higher-privilege account.
  3. Method coverage, not just GET. A resource might correctly block GET /api/reports/1234 for another user but forget to apply the same check on PATCH, DELETE, or a bulk-export endpoint that accepts an array of IDs.
  4. Indirect references. Some endpoints avoid predictable IDs but leak them elsewhere, for example a notification payload, a webhook body, or an admin-only listing endpoint that isn't properly access-controlled itself.
  5. State-changing side channels. Password reset flows, invite acceptance, and "unsubscribe" links frequently embed a token or ID that doubles as an authorization bypass if it isn't bound to the requesting session.

Automating the boring part

Manual testing doesn't scale past a handful of endpoints, and most real targets have hundreds. A basic differential authorization script pays for itself immediately: capture two sets of authenticated requests (as User A and User B), replay every request from A's session using B's resource IDs, and diff the response codes and bodies.

python607 Bytes
1import requests
2
3BASE = "https://target.example.com/api"
4USER_A_TOKEN = "..."
5USER_B_RESOURCE_IDS = ["a1b2c3", "d4e5f6", "091a2b"]
6
7endpoints = [
8 "/reports/{id}",
9 "/invoices/{id}",
10 "/messages/{id}/attachments",
11 "/users/{id}/settings",
12]
13
14headers = {"Authorization": f"Bearer {USER_A_TOKEN}"}
15
16for endpoint in endpoints:
17 for resource_id in USER_B_RESOURCE_IDS:
18 url = BASE + endpoint.format(id=resource_id)
19 r = requests.get(url, headers=headers, timeout=10)
20 flag = "!!! POSSIBLE IDOR" if r.status_code == 200 else ""
21 print(f"{r.status_code} {url} {flag}")

Run the same loop against PATCH, PUT, and DELETE with a harmless test payload (on a target where you're authorized to do destructive testing, ideally a program's dedicated test environment). A 200 or 204 where you expected a 403 is worth a manual follow-up every time, since automated tools produce false positives on endpoints that return 200 with an empty or generic body regardless of authorization.

Reading the response body, not just the status code

A subtler variant: the endpoint correctly returns 403 or 404, but the response body, headers, or timing still leak information. A 404 Not Found for "this report doesn't exist" versus a 404 for "this report exists but isn't yours" are supposed to look identical from the outside. If they don't (different error message, different response time, a Content-Length that varies based on whether the resource actually exists), that's an authorization oracle, and it's worth reporting even without a full data exposure, because it enables enumeration of valid resource IDs at scale.

Writing a report that gets triaged fast

The single biggest lever for IDOR report quality is a clean, reproducible before/after: two authenticated requests, side by side, one as the legitimate owner and one as an unauthorized account, with the resource ID being the only variable that changed. Include:

  • Both raw HTTP requests (method, full URL, relevant headers)
  • Both raw responses
  • A one-line summary of the access control that should have applied and didn't
  • The realistic worst case: is this read-only, or can the unauthorized user modify or delete data belonging to someone else?

That last point matters more than people think. "I can view another user's private report" and "I can delete another user's private report" are the same bug class with very different severities, and a good report always states which one you actually confirmed rather than assuming impact.

Why programs keep paying out for this

IDOR isn't a bug that gets "solved" once. It reappears every time a team ships a new endpoint, because authorization checks have to be reimplemented (or at minimum, re-verified) on every single resource-touching code path, while authentication is usually handled once in shared middleware. That asymmetry is exactly why systematic, endpoint-by-endpoint IDOR testing keeps finding real, impactful bugs on programs that have already been tested by dozens of other researchers. The bugs aren't hiding. They're just in the endpoint nobody got around to checking yet.

Share: