August 19, 2026
How to Find IDOR Vulnerabilities in Bug Bounty
A real walkthrough of how IDOR bugs get found in bug bounty, plus the exact methodology, tools, and testing steps to start finding them…

By b0dj0x
4 min read
A real walkthrough of how IDOR bugs get found in bug bounty, plus the exact methodology, tools, and testing steps to start finding them yourself.
IDOR is one of the first bug classes most hunters learn, and one of the last they ever stop finding. No exploit chains, no memory corruption, no custom shellcode. Just a request, a number, and a server that forgot to check who was asking.
This is a full breakdown of what IDOR actually looks like in practice, walked through as a real testing session, followed by the complete methodology I use to find it.
What IDOR Actually Is
Insecure Direct Object Reference happens when an app lets you reference an internal object (a user ID, an order number, a file, a ticket) and never verifies that you're allowed to touch that specific object. The app checks that you're logged in. It forgets to check that the thing you're asking for actually belongs to you.
It sits under OWASP's Broken Access Control category, which has topped the OWASP Top 10 in recent editions and remains the most reported vulnerability class in bug bounty programs today.
A Real Walkthrough
Here's what finding one of these actually looks like, start to finish, using a generic SaaS target as the example.
Step 1. Two accounts, always.
I register Account A and Account B on the target. Both fully verified, both with some real data in them (an order, a saved document, a message thread). Nothing gets tested without this pair.
Step 2. Generate normal traffic and watch the requests.
With Burp running, I click through every feature as Account A: dashboard, settings, billing, exports, sharing links, everything. I'm not looking for bugs yet. I'm building a map of every place an ID shows up.
A few minutes in, this request shows up when I open my billing history:
GET /api/v2/billing/invoices/48213
Authorization: Bearer eyJhbGciOi...GET /api/v2/billing/invoices/48213
Authorization: Bearer eyJhbGciOi...And this one when I export a report:
POST /api/v2/reports/export
{
"report_id": 9932,
"format": "pdf"
}POST /api/v2/reports/export
{
"report_id": 9932,
"format": "pdf"
}Both are candidates. Both go on the list.
Step 3. Swap the ID, keep your own session.
I log in as Account B, grab the invoice ID and report ID tied to that account, then go back to Account A's session in Burp Repeater and swap them in.
GET /api/v2/billing/invoices/48214
Authorization: Bearer <Account A's token>GET /api/v2/billing/invoices/48214
Authorization: Bearer <Account A's token>If the server responds with a 200 and Account B's actual invoice data, that's the bug confirmed. If it 403s or 404s, the endpoint checks ownership correctly and I move to the next one on the list.
In this example, the invoice endpoint returns the full billing record: name, address, amount, payment method. The export endpoint goes further. It doesn't just leak data, it generates a PDF of Account B's private report and hands it back to Account A. That's the difference between a low severity info leak and a critical one, and it only shows up because I tested every ID on the list, not just the obvious one.
Step 4. Confirm the pattern repeats.
One instance is a bug. A pattern is a better report. I check whether the same missing ownership check shows up on the DELETE and PATCH versions of the same endpoint, not just GET. In this case, DELETE /api/v2/reports/9932 also succeeds against Account B's report using Account A's token. That turns a data exposure finding into an unauthorized data destruction finding.
Step 5. Document it cleanly.
Two full request and response pairs (proof of A viewing B's data, and proof it's reproducible), a plain description of the missing check, and an honest severity assessment. That's the report.
The Methodology Behind It
That walkthrough is one endpoint. A real target has hundreds. Here's the full process I run against any new program.
Map every object reference
Go through the entire app and note everywhere an identifier appears, not just in the URL:
- Path segments:
/user/1024/profile - Query strings:
?invoice_id=48213 - JSON body fields:
"report_id": 9932 - Headers and cookies: custom headers like
X-Account-Id - Hidden form fields
- GraphQL query variables
- WebSocket payloads
- File paths:
/uploads/user_1024/file.pdf
Anything that references a specific object goes on the list before you test a single one.
Test every HTTP verb, not just GET
A missing check often only exists on one verb. GET might correctly block cross-account access while PUT or DELETE on the exact same object ID doesn't. Cycle through all of them.
Don't trust obfuscated IDs
If the app uses UUIDs or hashed values instead of sequential integers, that's not a fix, it's just a harder guess. Get a real ID belonging to Account B (through your own second account, not brute force) and test with it directly. Also check whether those IDs leak anywhere else: page source, API responses that populate a dropdown, PDF metadata, password reset links, referral URLs.
Trace multi-step flows, not just single endpoints
Some of the best findings hide in processes that span several requests, like generating a shareable link, inviting a teammate, or connecting a third-party account. An ID picked up in step two of a flow is often reused later without a fresh ownership check, even when the individual endpoints look safe in isolation.
Check GraphQL and mobile APIs separately
GraphQL resolvers sometimes skip access control that the frontend enforces visually. A query like user(id: "1024") { email } is worth testing directly against the API regardless of what the UI allows. Mobile apps often talk to a separate, less tested API version, so proxy your device or emulator traffic and repeat the same checks there.
Automate the repetitive parts
Manually swapping IDs across a large app doesn't scale. Burp's Autorize extension replays your session using a lower privileged token and flags anything that shouldn't have succeeded. Match and Replace can swap known Account B identifiers into your live traffic while you browse normally as Account A. For large ID ranges within scope, a short script comparing response status and length between an authorized and unauthorized session speeds things up considerably.
Tools Worth Setting Up
- Burp Suite, Repeater and Intruder for the manual work
- Autorize / Authorize, purpose built extensions for exactly this
- Postman or Insomnia, for working through documented API endpoints methodically
- Caido, a lighter proxy alternative to Burp
- Browser DevTools, for catching object references buried in frontend JS that never appear in the visible UI
Why Reports Get Marked Invalid
- No real cross-account impact shown, just a status code change with no actual data proven
- Testing against your own account through an unintended parameter (self-IDOR isn't a vulnerability)
- Data that's intentionally public, like public profiles or listings
- Sequential ID enumeration on an endpoint that's already been reported by someone else
Stick to your own test accounts, stay inside the program's scope, and confirm real impact before you submit.
Final Thoughts
IDOR pays off because the concept takes five minutes to understand and a lot of patience to hunt properly. The bug isn't hiding behind clever exploitation. It's hiding in the hundredth endpoint you check after the first ninety-nine came back clean. Map everything, test every verb, swap every ID, and the findings show up.
By b0dj0x.