---
name: Hunt IDOR
slug: hunt-idor
category: Quality
description: Hunt IDOR finds insecure direct object references and broken object level authorization issues in web apps, APIs, GraphQL, and multi-tenant services. Use it when testing for cross-tenant data access, object ID tampering, and field-level authorization gaps.
github: "https://github.com/H-mmer/pentest-agents/tree/main/skills/hunt-idor"
language: Python
stars: 804
forks: 156
install: "npx degit https://github.com/H-mmer/pentest-agents/tree/main/skills/hunt-idor ~/.claude/skills/hunt-idor"
installs_to: ~/.claude/skills/hunt-idor
source_path: skills/hunt-idor/SKILL.md
collection_size: 25
category_size: 1354
collection_url: "https://dirskills.com/collections/H-mmer/pentest-agents"
added: 2026-08-22T05:22:30.207Z
last_synced: 2026-08-22T05:22:30.207Z
canonical_url: "https://dirskills.com/skills/hunt-idor"
---

# Hunt IDOR

Hunt IDOR finds insecure direct object references and broken object level authorization issues in web apps, APIs, GraphQL, and multi-tenant services. Use it when testing for cross-tenant data access, object ID tampering, and field-level authorization gaps.

**Install:**

```bash
npx degit https://github.com/H-mmer/pentest-agents/tree/main/skills/hunt-idor ~/.claude/skills/hunt-idor
```

## README

## Crown Jewel Targets

IDOR (renamed BOLA in OWASP API1:2023) is the highest-frequency, second-highest-value bug class in modern bug bounty after RCE. ~40% of API attacks observed across production environments are BOLA per published research (Snyk 2026 Feb analysis). The 24-month meta has shifted decisively toward six asset types. All CVEs below are NVD-verified.

**1. Multi-tenant SaaS with client-supplied tenant context (CVSS 9.9 territory).** Every "send the tenant_id in the request" architecture is a candidate. **CVE-2026-30956** (OneUptime — `is-multi-tenant-query` header bypass + `projectid` header override → cross-tenant data exposure → reset token leak → ATO; GHSA-r5v6-2599-9g3m, CVSS 9.9 critical) is the canonical 2026 example. **CVE-2026-32131** (Zitadel Management API — low-priv `project.read` token reads other tenant's OIDC config; GHSA-wr6r-59xg-4pj2, affects 4.x through 4.12.1, 3.x through 3.4.7, 2.x through 2.71.19). **CVE-2025-64431** (Zitadel V2Beta Organization API — admin in Org A reads/modifies/deletes Org B; GHSA-cpf4-pmr4-w6cx, CVSS 8.7, fix in 4.6.3). Hunt header tenant injection on every multi-tenant target: `Tenantid`, `X-Org-Id`, `X-Tenant-ID`, `X-Project-Id`, `environmentId`, `is-multi-tenant-query`, `channel`. The OnSecurity disclosure ("How a single HTTP header unlocked every customer's data") documents the pattern in textbook form — `Tenantid: 3` to `Tenantid: 2` with no other change.

**2. Automotive / connected-vehicle platforms (six-figure-impact territory).** Sam Curry's pattern. **Kia 2024 disclosure** (samcurry.net/hacking-kia, Sep 2024) — dealer portal channel header manipulation → cross-account access → vehicle PII (name, phone, email, address) → silent secondary-user addition → remote unlock/start/track on any post-2013 Kia by license plate alone in 30 seconds. **Hyundai/Genesis/Honda/Nissan/Infiniti/Acura 2022-2023** (samcurry.net/web-hackers-vs-the-auto-industry) — same chain class against the entire auto industry. **Ferrari 2023** — full ATO + admin CMS access via IDOR on customer records + back-office endpoints. Hunt: dealer portals, fleet management APIs, telematics endpoints, OTA update orchestrators, EV charging networks. Bounties paid through automaker private programs and HackerOne IBB; impact framing pays mid-to-high five-figure when chained to physical vehicle control.

**3. GraphQL field-level / nested-object pivot (low-to-mid five-figure on enterprise SaaS).** GraphQL's resolver model means every field needs its own auth check, and most schemas miss them. **HackerOne $12,500 bounty Dec 2025** (Harshdranjan, documented by Monika Sharma writeup) — `certificationId` change in mutation deletes other users' Licenses & Certifications on hackerone.com itself. **$1,500 GraphQL field-level Feb 2026** (tinopreter Medium writeup) — `GetOrgWebhooks` query returns webhooks the user shouldn't see because field-level perms missing on `Project` accessed via `Organization` parent. **Yasser Hamoda April 2025 writeup** — unauthenticated GraphQL `user(username:"victim")` returns admin email/role with no auth. The pattern: any GraphQL endpoint where authentication is checked but field-level/object-level authorization isn't. Pivot endpoints: `me`, `user`, `organization`, `project`, `workflow`, `team`. Mutation IDOR (delete/update by ID) pays more than query IDOR.

**4. AI/ML platforms with cross-tenant model/data access.** New 2025-2026 surface, well-paying. **GHSA-3xx2-mqjm-hg9x (Paperclip Apr 2026, CVSS 10.0)** — board user in Company A mints agent API keys for any agent in Company B via `/agents/:id/keys`, then operates as that agent inside victim tenant — full cross-tenant compromise. **GHSA-gc8m-w37w-24hw (FastGPT)** — authenticated team accesses and executes any `appId` on `/api/v1/chat/completions` regardless of team ownership. **GHSA-2f4c-vrjq-rcgv (Tencent WeKnora)** — missing `tenant_id` WHERE clause in DB query tool exposes all tenants' API keys, model configs, private messages cross-tenant. The pattern: AI inference / agent management endpoints checking authentication but skipping tenant scoping.

**5. Government & enterprise legacy assets (DoD VDP through low five-figure on paid programs).** The H1 2024-2026 hacktivity is full of "IDOR exposes PII of tens of thousands" reports against forgotten asset surfaces. The 2026 Air Force candidate PII + recruitment chat logs disclosure (H1 critical) is a textbook example. Hunt: legacy CMS, candidate/recruitment portals, support ticket systems, file-upload migration endpoints, document-share systems.

**6. Apache Answer / Q&A / forum platforms with predictable token surface.** **CVE-2024-45719** (Apache Answer through 1.4.0, GHSA-mr95-vfcf-fx9p) — UUIDv1 timestamp-based tokens predict-by-arithmetic. The bananabr GitHub Security Lab disclosure (issue #816, paid via HackerOne #2513301 with linked bounty) introduced the CodeQL queries that catch this pattern systematically across JS/Python codebases. Hunt: any password reset, email confirmation, magic-link, or share-token implementation using UUIDv1 (timestamp-based) instead of UUIDv4 (random). The CodeQL query identifies sinks where `uuid.uuid1()` (Python) or `uuidv1()` (Node) flows into a token attribute — re-run against any in-scope OSS target.

**Financial APIs with per-account state IDOR.** Sri Sowmya Nemani Sep 2025 financial-services writeup — `account_number` parameter override returns other users' onboarding/funding state without PII but with regulatory-grade privacy violation. The pattern: any API where the account / customer identifier is in the request body or path and isn't checked against session ownership. Pays high four-figure to low five-figure on most fintech programs even without PII when state-disclosure has compliance implications (GDPR, GLBA, PCI).

**SCIM / IdP / IAM endpoints.** SCIM is a magnet for IDOR because the spec encourages identifier-driven update operations. **Keycloak SCIM PUT body ID override** (issue #46658, Feb 2026) — `ScimResourceTypeResource.update()` validates URL `{id}` exists, then calls `update()` with the body's `id` field, allowing path-vs-body mismatch attack to update any SCIM-managed resource. Hunt every SCIM `/Users/{id}` and `/Groups/{id}` PUT for path-body consistency.

**What pays the most:** unauthenticated cross-tenant data exposure (low-to-mid five-figure on enterprise SaaS); IDOR chained to ATO via leaked password reset tokens (mid five-figure when proven); admin-account IDOR on multi-tenant platforms (mid four-figure to low five-figure); destructive IDOR (delete/modify other users' resources, low five-figure on $12.5k HackerOne case); financial state IDOR (high four-figure to low five-figure on fintech programs even without PII). Account-state IDOR alone is generally low four-figure to mid four-figure unless chained.

## Attack Surface Signals

Greppable signals that this surface might exist:

```bash
# Sequential ID surface in URL paths (IDOR candidates)
rg -n '/(users?|orders?|invoices?|tickets?|files?|reports?|projects?|workflows?|certifications?|teams?|agents?)/[0-9]{1,8}\b' \
   --type js --type ts --type py --type go --type rb

# UUID v1 (timestamp-predictable, CVE-2024-45719 family) generation
rg -n 'uuid\.uuid1\(\)|uuidv1\(\)|UUID\.randomUUID\(\)\.toString\(\).*timestamp|UuidV1' \
   --type py --type js --type java

# Tenant context in headers / body (BOLA via header swap)
rg -n -i '(tenantid|tenant_id|tenant-id|x-org-id|x-tenant-id|x-project-id|environmentid|is-multi-tenant)' \
   --type js --type ts --type py --type go

# MongoDB queries missing organization filter (Novu pattern)
rg -n 'findOne\(\{[^}]*_id[^}]*\}' --type js --type ts | rg -v '_organizationId|_orgId|organization:'

# SQL queries missing tenant_id WHERE clause (WeKnora pattern)
rg -n 'SELECT.*FROM\s+\w+\s+WHERE\s+id\s*=' --type py --type java --type rb | rg -v 'tenant_id|org_id'

# GraphQL resolvers without context.user check (field-level auth missing)
rg -n -B 2 -A 8 '@ResolveField|resolveField|resolver.*\(.*\):' --type ts --type js | \
   rg -v 'context\.user|context\.auth|requireAuth|@AuthGuard'

# SCIM endpoints (path vs body ID mismatch — Keycloak issue #46658)
rg -n '/scim/v2/(Users|Groups)/' --type java --type js
rg -n 'ScimResource.*update' --type java

# Mass-assignment unsafe binding (BOLA's cousin)
rg -n 'request\.body|req\.body|@RequestBody' --type js --type ts --type java | rg -v 'pick\(|allowedFields|Allowlist|@JsonIgnore'
```

HTTP-level signals on a live target:

- Sequential numeric IDs in any path (`/api/v1/users/123`, `/orders/4532`) → **classic IDOR** — try ±1 enumeration first
- `Tenantid: 3`, `X-Org-Id: <id>`, `X-Tenant-ID:`, `X-Project-Id:`, `environmentId:` headers → **client-supplied tenant context** (OneUptime CVE-2026-30956 pattern; Novu GHSA-323c-xqcq-fpcp pattern) — swap value, replay
- `is-multi-tenant-query: true` header in any response trace → **CVE-2026-30956 OneUptime header bypass** — toggle and replay
- `channel:` request header on automotive / dealer portal traffic → **Sam Curry Kia 2024 chain** — modify channel header to bypass dealer-vs-customer permission tier
- GraphQL endpoint `/graphql` or `/api/graphql` reachable + introspection enabled → **GraphQL IDOR field-level surface** — enumerate types, look for `user(id:)` / `user(username:)` / `organization(id:)` queries
- POST/PUT/PATCH bodies containing both URL path identifier AND a body `id` field → **path-vs-body mismatch IDOR** (Keycloak SCIM #46658, very common in REST→DB ORM patterns)
- UUID v1 in any token (decode via tools.bytestream.com — first 60 bits are timestamp) → **CVE-2024-45719 family** — predict adjacent UUIDs by arithmetic
- `dealer.kia.com`, `connect.kia.com`, `dealer.honda.com`, `myhyundai.com`, automotive OEM dealer/connect domains → **Sam Curry pattern targets**
- Server header reveals `Apache Answer`, `Indico`, `Zitadel`, `OneUptime`, `Novu`, `FastGPT` → **specific NVD-verified IDOR CVE**
- `aws-region:` / `region:` body fields in inference / CDN APIs → **region-as-tenant** misconfig
- `403 Forbidden` for some objects of one type but `200 OK` for adjacent IDs of same type → **inconsistent authorization** = BOLA candidate
- GraphQL response with introspection schema present (`__schema`, `__type` in response) → **schema-discovery IDOR** — read schema, find sensitive fields, query directly
- SCIM endpoints `/scim/v2/Users/{id}` reachable with low-priv token → **Keycloak issue #46658 path-body override** — try PUT with mismatched body id

## Insertion Point Taxonomy

Every place attacker-controlled identifiers flow for IDOR/BOLA:

- **URL path** — `/users/<id>`, `/api/v2/workflows/<id>`, `/scim/v2/Users/<id>`. Most common. Try ±1, UUID swap from another response, null UUID `00000000-0000-0000-0000-000000000000`.
- **URL query** — `?id=`, `?user_id=`, `?account_number=`, `?environmentId=` (Novu CVE pattern), `?targetEnvironmentId=` (Novu PUT variant).
- **Custom headers** — `Tenantid`, `X-Org-Id`, `X-Project-Id`, `X-Tenant-ID`, `is-multi-tenant-query` (OneUptime), `channel` (Sam Curry Kia), `X-User-Id`, `X-Account-Id`, `aws-region` (region-as-tenant pattern).
- **Body fields** — `id`, `user_id`, `tenant_id`, `org_id`, `project_id`, `account_number`, `certificationId` (HackerOne $12.5k case), `appId` (FastGPT GHSA-gc8m-w37w-24hw), `environmentId` (Novu PUT body variant).
- **Body include/expand** — `include_tenants:["victim-corp"]`, `expand:["organization"]`, `relations:["other_user"]` — fields that opt into joined data without re-checking permission.
- **JWT claims** — `sub`, `tenant_id`, `org_id`, `roles[]`. Try claim swapping if signature verification is missing or weak. OnSecurity write-up on Tenantid header notes this as the proper fix the vendor missed: derive tenant from JWT claim, not request.
- **GraphQL variables** — `{user(id: $id)}`, `{organization(id: $id) {projects {id, sensitiveField}}}`. Field-level pivot via nested objects (tinopreter Feb 2026 case: query `Organization.project` instead of `Project` directly).
- **GraphQL nested object pivots** — when direct `project(id:)` is blocked, query `organization(id:) { projects { ... } }` because the org-level resolver doesn't re-check project permissions.
- **GraphQL field selection** — request `token`, `resetPasswordToken`, `permissions`, `email`, `internalNotes` fields on user objects you don't own (Yasser Hamoda 2025 case: requesting `role` field on `user(username:victim)`).
- **Cookies** — session-bound IDs (`tenant_session=acme-corp`), customer-id cookies, multi-tenant subdomain mappings.
- **WebSocket frames** — IDOR via JSON message handlers, often missed by HTTP-only review. Subscribe to other tenant's channel by sending crafted subscription frame.
- **Background/async paths** — export jobs, report generation, notification processing. Job queue entries often process without re-validating tenant context. Inject your job entry pointing at victim's data; the worker writes the output to your output bucket but reads from victim's data.
- **File paths in upload/download** — `/api/files/<id>/download`, `/uploads/<filename>`. If filename is sequential/predictable, IDOR. If filename is UUID, check UUID version.
- **SCIM resources** — `/scim/v2/Users/<id>` with body containing `id` — Keycloak issue #46658 path-body mismatch.
- **Inference / agent endpoints** (AI/ML targets) — `appId` (FastGPT), `agentId` (Paperclip), `modelId` (WeKnora). Include the victim's ID in the path/body and watch the response.

For each surface, send: your own ID, victim's ID, ID±1, null UUID, your ID with victim's `tenant_id` header, victim's ID with your `tenant_id` header. Watch for `200` instead of `403`.

## Step-by-Step Hunting Methodology

1. **Two accounts always.** IDOR hunting requires victim and attacker accounts in the target system. If the program is private and you can only have one account, focus on cross-tenant via header injection / unauthenticated endpoints. Without two accounts, you cannot prove most BOLA findings.

2. **Map the entire API surface.** Crawl JS bundles, Swagger/OpenAPI specs (`/swagger.json`, `/api/v1/openapi.json`, `/.well-known/openapi`), mobile app HTTPS traffic (Frida, mitmproxy on simulator), Postman collections. Look for endpoints the UI doesn't expose. The hidden endpoints are where IDOR lives because the hunters before you didn't see them.

3. **Identify all object identifier types.** For each endpoint, note: integer? UUID v4? UUID v1 (timestamp-predictable, CVE-2024-45719 family)? Slug? Encoded? Hash? UUID v1 → **immediate UUID prediction attack** (decode timestamp from first 60 bits, predict adjacent IDs). Sequential integer → enumeration attack. Slug → guess from public data (usernames, project names).

4. **Check tenant header surface first on multi-tenant targets.** If you see ANY of `Tenantid`, `X-Org-Id`, `X-Tenant-ID`, `X-Project-Id`, `environmentId`, `is-multi-tenant-query`, `channel` in request headers — your hunting starts there. Swap the value to another tenant ID (sequential? guess. UUID? get from another response or a friend's account). The OneUptime CVE-2026-30956 / Novu GHSA-323c-xqcq-fpcp / Sam Curry Kia chain all start at this step.

5. **Test every HTTP method on every endpoint.** GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS. The READ IDOR is mid four-figure; the DELETE IDOR is mid five-figure (HackerOne $12,500 GraphQL case was DELETE on certifications). The PATCH IDOR opens mass-assignment territory (BOLA's cousin).

6. **For GraphQL targets: introspect, then pivot through nested objects.** Send `{__schema { types { name fields { name } } }}` to enumerate. Find sensitive types (`User.email`, `User.role`, `User.token`, `Organization.apiKey`). For each, try direct query (`{user(id: <victim>) { email }}`) AND nested via parent (`{organization(id: <yours>) { users { email }}}`). The nested pivot usually works when the direct query is blocked because resolver auth is per-type, not per-field. Reference: $1,500 HackerOne disclosed Feb 2026 writeup at https://medium.com/@tinopreter/1-500-pii-leak-via-graphql-field-level-permission-bypass-1e7ea2d1a019, Yasser Hamoda April 2025 HackerOne disclosure.

7. **For mutations: change every ID in the body.** GraphQL mutations + REST PUT/PATCH/DELETE. If a mutation takes `certificationId: 123`, replay with `certificationId: 124` (HackerOne disclosed $12,500 case Dec 2025, Harshdranjan via medium.com/h7w writeup). Watch for `success` responses without permission errors.

8. **Path-body ID mismatch attack on REST.** When PUT/PATCH endpoints take both URL path id and body id, send mismatched values: `PUT /resources/MY_ID` with body `{"id": "VICTIM_ID", ...}`. The Keycloak SCIM issue #46658 (Feb 2026) is the textbook case — URL path validates existence, body id is what actually gets updated.

9. **JWT claim manipulation.** Decode the JWT, identify the tenant/role/sub claim, modify, re-encode. Three failure modes pay: (a) signature not verified at all (`alg: none`), (b) signature verified with attacker-known secret, (c) signature verified but claim isn't checked server-side (the `tenant_id` in JWT is decorative; server uses request header). The OnSecurity Tenantid disclosure documents (c) — the server should derive tenant from JWT but uses the request header.

10. **Mass assignment on PATCH/PUT.** Submit fields the UI doesn't show: `role`, `is_admin`, `is_verified`, `subscription_tier`, `credit_balance`, `permissions[]`, `tenant_id`. Check via subsequent GET — the mass-assigned field may stick even if the response doesn't show it.

11. **Background-job IDOR.** Find async paths (export, report-gen, notification, scheduled). Trigger an export job on victim's resource ID; worker may process without tenant scoping. Output lands in your bucket because writer uses your context, but reader uses the supplied resource id.

12. **AI/ML cross-tenant.** For inference/agent endpoints, supply victim's `appId` / `agentId` / `modelId`. Three failure modes: (a) auth checked, ownership skipped (FastGPT GHSA-gc8m-w37w-24hw); (b) cross-tenant key minting (Paperclip GHSA-3xx2-mqjm-hg9x — your call mints a key with victim's `companyId` claim); (c) DB query missing tenant filter on AI-data tables (Tencent WeKnora GHSA-2f4c-vrjq-rcgv — `models`, `messages`, `embeddings` not in tenant-isolation list).

13. **Validate before reporting.** Two accounts, two screenshots, both Burp request/response pairs side-by-side, redacted PII. Don't dump 100k records — the report needs ≤3 victim records to prove it (one is enough for most triagers). See Gate 0.

## Payload & Detection Patterns

### Sub-technique A — Sequential ID enumeration

```
# Direct path enumeration
GET /api/v1/users/1
GET /api/v1/users/2
...
# Use ffuf for fast brute force (auth header preserved)
ffuf -u https://target/api/v1/users/FUZZ -H "Authorization: Bearer <yours>" \
     -w /usr/share/wordlists/seclists/Fuzzing/numbers.txt -mc 200 -fs 0

# Burp Intruder cluster bomb on numeric ID
GET /api/v1/orders/§1§
Authorization: Bearer <yours>

# Compare responses by length
# Same length → likely 403/404; different length → likely 200 with other data
```

### Sub-technique B — UUID v1 timestamp prediction (CVE-2024-45719 family)

```python
# Decode UUIDv1 to extract timestamp + node
import uuid
u = uuid.UUID('your-uuid-here')
print(f"Version: {u.version}")  # 1 = timestamp-based, vulnerable
print(f"Time: {u.time}")  # 100ns intervals since 1582-10-15
print(f"Node: {u.node:012x}")  # MAC address of generator

# Predict adjacent UUIDs (target generated 1000 tokens/sec)
# Step 1: get a UUID for a known timestamp (e.g., trigger your own password reset)
your_uuid = uuid.UUID('xxxxxxxx-xxxx-1xxx-xxxx-xxxxxxxxxxxx')
your_time = your_uuid.time
your_node = your_uuid.node

# Step 2: generate UUIDs for nearby timestamps
import struct
predicted = []
for delta in range(-1000, 1001):  # 1000 ticks = 100us window
    t = your_time + delta
    # Construct UUIDv1 with same node, different timestamp
    time_low = 
