
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.
Security research and platform engineering at HackerSavanna.
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:
- Horizontal escalation. Two accounts, same privilege level. Account A tries to read, update, and delete Account B's resources.
- Vertical escalation. A low-privilege account (free tier, unverified researcher, guest role) attempts actions that should require a higher-privilege account.
- Method coverage, not just GET. A resource might correctly block
GET /api/reports/1234for another user but forget to apply the same check onPATCH,DELETE, or a bulk-export endpoint that accepts an array of IDs. - 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.
- 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.
1import requests2 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.
Related Posts

Cloud Storage Misconfigurations: Hunting Exposed S3 and GCS Buckets
A field guide to discovering, safely confirming, and accurately assessing impact on publicly exposed object storage across every major cloud provider.

Reverse Engineering Android Apps for Hardcoded Secrets and Insecure Storage
A hands-on walkthrough of decompiling an APK, hunting for embedded credentials, and finding exported components that leak more than they should.