Comprehensive IDOR Guide

A practitioner’s reference for Insecure Direct Object Reference (IDOR) and Broken Object Level Authorization (BOLA) — attack surface, enumeration patterns, bypass techniques, real-world writeups, detection workflow, and prevention. Compiled from 21 research sources.


Table of Contents

  1. Fundamentals
  2. IDOR vs BOLA vs BFLA
  3. Attack Surface & Where Identifiers Live
  4. Horizontal vs Vertical Access
  5. Identifier Enumeration Patterns
  6. Parameter Tampering Techniques
  7. HTTP Method & Verb Tampering
  8. Content-Type & Format Bypasses
  9. Path, Version, and Endpoint Tricks
  10. Mass Assignment Overlap
  11. UUID & Unpredictable ID Defeats
  12. Second-Order and Blind IDOR
  13. GraphQL, WebSocket, and Non-REST Surfaces
  14. Real-World Writeups & CVEs
  15. Exploit Chains
  16. Detection Methodology with Autorize
  17. Tools & Automation
  18. Impact & Severity Mapping
  19. Prevention & Secure Design
  20. Testing Checklist
  21. Report Writing

1. Fundamentals

IDOR occurs when an application uses user-supplied input to reference an internal object (database row, file, resource) and fails to verify whether the current user is authorized to access that specific object. The application trusts the identifier, not the identity.

At its core, IDOR is a missing-authorization bug. Authentication answers “who am I?” IDOR-class bugs live in the failure to answer “what am I allowed to touch?”

The shape of an IDOR:

GET /api/users/12345/orders      <-- you own 12345
GET /api/users/12346/orders      <-- 12346 belongs to someone else
                                     200 OK => IDOR

Three ingredients are required:

  1. A direct reference to an object (numeric ID, UUID, hash, slug, filename, path)
  2. A server-side operation (read, write, delete, invoke) that consumes the reference
  3. Missing or incomplete ownership/permission check between request identity and target object

Impact spectrum: PII exposure → private data modification → account takeover → privilege escalation → full multi-tenant compromise.

IDOR is a logic bug. There is no single signature, no single framework fix, and no blanket scanner rule. It requires a human eye, two accounts, and systematic replay.


2. IDOR vs BOLA vs BFLA

The terminology overlaps. All three describe broken access control but at different granularity.

TermMeaningOWASP
IDORClassic — user-supplied reference accesses an object without ownership checkLegacy OWASP Top 10 (A4:2007)
BOLABroken Object Level Authorization — same bug, API-focused namingOWASP API Top 10 #1
BFLABroken Function Level Authorization — regular user invokes admin-only functionOWASP API Top 10 #5
Mass AssignmentClient submits extra fields (role, isAdmin, accountId) that the server blindly bindsOWASP API Top 10 #6

BOLA = “can I read user 999’s data?” BFLA = “can I call /admin/deleteUser as a regular user?” Mass Assignment = “can I add "role":"admin" to my profile update?” All three live under the access-control umbrella and are routinely chained.


3. Attack Surface & Where Identifiers Live

Build a mental map of where references show up before you start flipping values.

Common identifier locations

LocationExamples
URL path/api/invoices/98432, /users/johndoe, /documents/view/a1b2c3d4
Query string?userId=5001, ?account=ACCT-112233, ?fileId=xyz789
Request body (JSON/XML/form){"recipientId":"user_abc","organizationId":"..."}
Custom headersX-User-ID, X-Account, X-UID, W-User-Id, User-Token
Cookiesuid=5001, session tokens that encode user ID
Referer / OriginIDs leaked into referrer on cross-link navigation
GraphQL variablesmutation UpdateSound(input:{uuid:"..."})
WebSocket frames{"action":"subscribe","channelId":"user_5001"}
Hidden form fields<input type="hidden" name="user_id" value="12345">
JWT claimsServer trusts sub or custom claim without re-verifying resource ownership

High-value feature areas

These features are where IDOR bounties concentrate:

  • Account management — profile update, email change, password reset, 2FA settings
  • Billing & payments — stored cards, invoices, subscription management, refunds
  • File operations — upload, download, share, copy, move, delete
  • Messaging — direct messages, chat invites, group membership, draft auto-save
  • Export / reporting — scheduled exports, report generators, downloads
  • Org/tenant settings — SSO config, team members, API tokens, role assignments
  • New features — anything shipped in the last release cycle (less-tested paths)

4. Horizontal vs Vertical Access

TypeAttackerTarget
HorizontalUser A (same role)User B’s objects at the same privilege level
VerticalLow-privilege userAdmin/higher-role functions or objects
UnauthenticatedAnonymousAny authenticated user’s objects
Cross-tenantUser in org AObjects inside org B (the common SaaS variant)

Unauthenticated IDOR is the jackpot — no account required, mass-scrape-ready, and almost always critical. Always retry requests with the Authorization header stripped.

Cross-tenant IDOR is the modern variant. A user inside tenant A changes the organizationId in a PUT body and writes into tenant B’s config. This is exactly the Flowise CVE-2025-XXXX pattern — the endpoint checked authentication but not which org the JWT belonged to.


5. Identifier Enumeration Patterns

Sequential integers

The simplest, most paid bug class. Increment, decrement, and probe around your own ID.

  • Don’t just go +1 / -1. Try small IDs (1, 2, 3) — admin accounts often sit at the bottom.
  • Try 0, -1, 00001234 (leading zeros), 1234.0, 1e3.
  • Scrape press releases, public profiles, and disclosed writeups for real victim IDs.

UUIDs and hashes — still reachable

UUIDs are not access control. They are defense in depth against guessing. You still win when:

  • UUIDs leak in public profile URLs, share links, invite emails, or profile pictures
  • UUIDs appear in API responses you already receive (friends list, comments, mentions)
  • The same UUID gets reused across resources (one ID, many endpoints)
  • Old Wayback Machine snapshots expose them
  • Google/Bing dorks find indexed copies
  • Sign-in / password reset flows return UUIDs on error
  • In-app sharing links, email unsubscribe links, @mentions

Encoded IDs

Decode them. Try:

/profile/MTIzNDU2           -> base64("123456")
/file/5d41402abc4b          -> MD5("hello")
/account/7541A92F-0101-...  -> try substituting numeric 1234

If a portion of the encoded value is static and a portion changes linearly across IDs you own, you have a predictable namespace.

me / current / self aliases

If the API takes /api/users/me, try swapping me for a numeric ID (/api/users/1234). Apps often support both forms and only one checks authorization.

ID references scraped from the app itself

Before brute forcing, harvest IDs from within the authenticated app:

  • JavaScript bundles (look for hard-coded IDs, admin panels, dev endpoints)
  • HTML source of list/index pages
  • .map source maps
  • API responses that enumerate resources for pagination
  • Autocomplete endpoints (/api/users/search?q=)
  • Error messages that echo IDs

6. Parameter Tampering Techniques

When a straight ID swap returns 403 or 404, mutate the parameter shape.

ID value mutation table

TechniqueExampleWhy it works
Wrap in array{"id":19}{"id":[19]}ORM unwraps array; auth check ran on unwrapped scalar
Array of targets{"id":[1234,1235]}Check runs on first element only
Negativeid=-1Backend casts; comparator fails
Zeroid=0Sometimes returns the “default” / first record
Large int / leading zerosid=00001234Parser differential between validator and DB driver
Floatid=1234.0Type coercion skips check
String with delimiterid="1234,1235"Comma-split on server side
Booleanid=trueRarely, maps to record 1
Wildcardid=*, id=%Some ORMs interpret literally
Null byteid=1234%00Truncates in certain string handlers
Extra keyadd user_id alongside idUndocumented override
Rename keyalbum_idaccount_idUnexpected param honored unchecked

HTTP Parameter Pollution (HPP)

Supply the same parameter twice and see which side of the stack each copy reaches:

GET /api/account?id=<your id>&id=<admin id>

Different servers pick differently:

  • Apache/PHP → first value
  • Tomcat/Java → first value (often)
  • .NET → concatenated with comma
  • Node.js (Express default) → array
  • Rails → last value

If the authorization middleware reads the first copy and the data layer reads the last copy, you have HPP-to-IDOR.

Add IDs to requests that don’t have them

Endpoints that implicitly use your session user are often the juiciest. Try adding explicit IDs:

GET /api/MyPictureList
GET /api/MyPictureList?user_id=<victim_id>

The server may honor the unexpected parameter and skip its “current user” shortcut.

Client-supplied IDs on create

Some apps let the client specify the primary key on create. Try supplying one:

POST /api/notes  {"id":"attacker-chosen","content":"..."}

If accepted, you can choose IDs that collide with other users, trigger reference reuse, or squat on predictable future IDs.

Parameter reuse across endpoints

If album_id works on /api/albums, try the same name on /api/photos, /api/comments. Paramalyzer (Burp extension) tracks parameter names used on a host and hints at reuse opportunities.


7. HTTP Method & Verb Tampering

Access control is often wired only to the method the client actually uses. Try every verb against every endpoint.

OriginalTry
GET /api/users/{id}POST, PUT, PATCH, DELETE, HEAD, OPTIONS
POST /api/messagesPUT, PATCH
PUT /api/profilePOST (create), DELETE

Intigriti’s “Complete Guide” explicitly calls this out: two endpoints can exist under the same path, and only the documented one has the auth middleware attached. A POST to a GET-only URL may hit an orphaned handler.

Also try:

  • X-HTTP-Method-Override: PUT header on a POST body
  • _method=DELETE query parameter (Rails/Laravel accept this)
  • Lowercase method (get vs GET) — rare but seen

8. Content-Type & Format Bypasses

Servers often route requests to different parsers by Content-Type, and only one parser path has authorization. Try swapping:

OriginalTry
application/jsonapplication/xml, text/xml, text/x-json, application/x-www-form-urlencoded, multipart/form-data
application/xmlapplication/json, text/plain
application/x-www-form-urlencodedapplication/json, text/plain

Example from the Aon writeup:

POST /api/chat/join/123
Content-Type: application/xml       -> Content-Type: application/json
<user>test</user>                   -> {"user":"test"}

Also try swapping file extensions on resource URLs:

/api/reports/55          -> /api/reports/55.json
                         -> /api/reports/55.xml
                         -> /api/reports/55.config
                         -> /api/reports/55.xlsx

Some frameworks serve the same controller via extension-based content negotiation and skip auth filters on the alternate route.


9. Path, Version, and Endpoint Tricks

API versioning

New code lives in /v2/, but /v1/ is still routable and never got the patch:

/api/v1/users/{id}     <-- vulnerable, forgotten
/api/v2/users/{id}     <-- fixed

Try v0, v1, v2, v3, beta, alpha, internal, admin, legacy.

Sibling endpoint discovery

If you see /api/albums/{album_id}/photos/{photo_id}, guess neighbors:

/api/albums/{album_id}/share
/api/albums/{album_id}/export
/api/albums/{album_id}/audit
/api/albums/{album_id}/metadata
/api/users/{user_id}/details
/api/users/{user_id}/delete

Wordlists: Burp Intruder, ffuf with API-specific lists (arjun, api-endpoints-res), CeWL for app-specific wordlists.

Hidden documentation

  • Swagger / OpenAPI (/swagger, /api-docs, /v2/api-docs, /openapi.json)
  • GraphQL introspection (__schema)
  • WSDL (?wsdl)
  • .js bundles with route tables
  • robots.txt / sitemap.xml
  • Wayback Machine snapshots of old JS

The hipotermia smuggling+IDOR chain started by finding a hidden Swagger UI exposing the full endpoint catalog.

Trailing slashes, case, path params

  • /api/users/123 vs /api/users/123/
  • /API/Users/123 (some servers route case-insensitively, filters don’t)
  • /api/users/123/.. (traversal side effects)
  • /api/users/123%2F.. (URL-encoded traversal)
  • /api/users/123# (fragment truncation)
  • /api/users/123;admin (path parameters)

10. Mass Assignment Overlap

Mass assignment is IDOR’s evil twin. Instead of flipping the reference ID, you inject extra fields that the server blindly binds to the target object.

// Normal
PUT /api/users/me  {"name":"Alice"}

// Mass assignment
PUT /api/users/me  {"name":"Alice","role":"admin","isVerified":true,"accountBalance":999999,"organizationId":"victim-org"}

Fields to probe:

  • role, isAdmin, admin, permissions, groups
  • verified, emailVerified, isActive, status
  • balance, credit, points, tier, plan
  • ownerId, userId, accountId, organizationId, tenantId
  • createdAt, updatedAt (time-travel attacks)
  • deleted, deletedAt (un-delete)
  • password, passwordHash (direct password set)

Discovery trick: fetch the resource with GET, copy the full response body into a PUT, and resubmit. Server often accepts every field it returned and applies them all. This trivially exposes hidden fields.

The Flowise CVE bundles IDOR + mass assignment + business logic: the organizationId in the body chooses the target (IDOR), the providers array blindly overwrites SSO config (mass assignment), and missing license enforcement lets free-tier users enable enterprise features (business logic).


11. UUID & Unpredictable ID Defeats

UUIDs raise the bar but don’t end the game.

Harvesting UUIDs

SourceTechnique
Profile pictures<img src="...avatars/UUID.jpg"> embedded on public profile
Share linksCopy-share function returns link with UUID
Password resetReset endpoint returns UUID in JSON error/success
Invitations“User UUID not found” error messages
In-app searchAutocomplete returns {id: UUID, name: ...}
Wayback MachineHistorical API responses with UUIDs cached
Search enginessite:target.com "uuid" dorks
Email unsubscribe/unsubscribe?uid=UUID links
Error pagesStack traces echoing object IDs

Predictable “random”

  • v1 UUIDs encode MAC address + timestamp — you can predict adjacent IDs if you know one
  • Incrementing snowflakes (Twitter/Discord-style) are sortable by time
  • Sequential within a shard — many “random” IDs are only random per table

Leaked via second endpoint

The two-endpoint trick: one endpoint requires a UUID you don’t know, but a different endpoint returns a list of them. Intigriti’s example: search/enumerate endpoint spits back UUIDs, target endpoint consumes them.


12. Second-Order and Blind IDOR

Second-order IDOR

Your input is stored first, then retrieved later in a privileged context with no re-check. Classic example: scheduled export jobs.

  1. Create an export job {"scheduleFor":"tomorrow","userId":"me"}
  2. Job gets stored with attacker-controlled userId
  3. Scheduler worker runs next day, reads userId from job metadata, generates export
  4. Second step never re-validates that the scheduler’s caller owns userId

Also seen in:

  • Webhook delivery (stored target URL, later consumed server-side)
  • Cron/reminder systems
  • Audit/log replay
  • “Retry failed job” buttons

Blind IDOR

The HTTP response is generic (success/error) and doesn’t leak the victim data, but the action still took effect. Detection:

  • Check your own email — did the state change produce an out-of-band notification?
  • Log into the victim account — did something actually change?
  • Look for side channels: push notifications, SMS, audit logs, activity feeds
  • Timing differences between “hit” and “miss”

13. GraphQL, WebSocket, and Non-REST Surfaces

GraphQL IDOR

GraphQL is underexplored territory. Resolvers frequently attach auth at the query root but not at nested resolvers.

query {
  user(id: "victim_id") {
    email
    orders { total, items }
    paymentMethods { last4 }
  }
}

Checklist:

  • Run introspection (__schema, __type) — if enabled, the whole attack surface is handed to you
  • Test every Query.<resource>(id:) field with victim IDs
  • Test every Mutation.update* / delete* with victim IDs
  • Look for nodes(ids:[...]) batched fetchers — auth check often misses array members
  • Alias attacks: query the same field N times with different IDs in one request
  • Relay-style node(id:"...") global resolver — often a universal IDOR vector

WebSocket

Real-time apps ship IDs over WS frames. Intercept with Burp’s WebSockets history:

{"action":"subscribe","channelId":"user_5001"}
{"action":"join","roomId":"private-chat-abc"}

Swap channel/room IDs. Presence, DMs, and notification streams are common blind spots because testers focus on HTTP.

gRPC / protobuf

Same rules, different wire format. Use grpcurl or Burp with a grpc-web decoder. The ID is still in the request — just binary-encoded.


14. Real-World Writeups & CVEs

Flowise /api/v1/loginmethod — Cross-tenant SSO takeover

Class: IDOR + mass assignment + business logic bypass

A PUT to /api/v1/loginmethod accepted organizationId in the request body and updated the matching org’s SSO config. Middleware checked authentication but not whether request.user.organizationId === body.organizationId. A free-tier attacker could overwrite any org’s Google OAuth client ID with their own, hijacking every victim employee login.

Pattern: authenticated-but-unauthorized cross-tenant config write.

Detection heuristic: grep every PUT/POST handler for request-body fields named *Id/*organizationId/*tenantId and verify they’re validated against the caller’s scope.

Nginx UI CVE-2026-33030

Class: Classic horizontal IDOR (architectural)

The Go base Model struct lacked a user_id field. All resource queries selected by resource ID alone, never joining on ownership. Any authenticated user could read, modify, or delete any other user’s configs. CVSS 8.8.

Lesson: when the ORM base model has no ownership column, every derived model is structurally IDOR-prone. Grep for Model.find(id) vs current_user.Model.find(id).

Dubsmash UpdateSound GraphQL mutation

Class: GraphQL IDOR on a mutation

The UpdateSound(input:{uuid,name}) mutation updated any soundtrack by UUID without checking ownership. Soundtrack UUIDs were exposed in feed and search responses, so the attacker could rename any track in the library. $3,000 bounty.

Lesson: GraphQL mutations with a resource identifier in the input need explicit ownership checks at the resolver level — the Authorization header tells you who’s calling, not what they own.

HTTP Request Smuggling + IDOR (hipotermia writeup)

Class: Chained — CL.TE desync escalates a “nobody-cares” IDOR into session hijack

An unauthenticated endpoint POST /addCard/{userId} added a card to whatever user ID you supplied — which alone is useless (why give someone else your card?). Chained with CL.TE request smuggling: attacker smuggles a POST /addCard/<attacker_id> prefix onto the victim’s next request. When the victim’s legitimate POST with their real card details flows through, the smuggled prefix redirects it — the victim’s card lands on the attacker’s account.

Lesson: “low impact IDOR” often becomes critical when chained with a request modifier. Request smuggling, cache poisoning, CSRF, and open redirect are the usual force multipliers.

CVE-2023-4836 — User Private Files WordPress plugin

Low-privilege users could craft requests to download any user’s uploaded files by manipulating the file reference. Classic file-path IDOR with no user ownership check on the download endpoint.

Reddit password reset / X-User-ID header IDORs

From the Bugcrowd writeup: debug headers like X-User-ID, X-UID, X-Account-Id left over from test environments were honored in production. Setting the header to a victim’s ID performed actions as that user — full ATO via header injection. Always fuzz X-User-* / X-*-Id headers.


15. Exploit Chains

IDOR rarely travels alone on a maximum-severity report. Common chains:

ChainEscalation
IDOR → ATOIDOR on password reset, email change, or 2FA enrollment endpoint
IDOR + Self-XSSUse IDOR to plant XSS payload in victim’s profile, upgrades self-XSS to stored XSS
IDOR + Mass AssignmentIDOR targets the user, mass assignment sets role=admin
IDOR + CSRFVictim’s browser fires the IDOR request to attacker-chosen target
IDOR + Request SmugglingSmuggling prepends IDOR request onto victim connection
IDOR + Business LogicBypass tier/license checks by writing to org config directly
BFLA + BOLACall admin function on victim’s object (delete, suspend, impersonate)
IDOR on invite → org takeoverAccept someone else’s pending org invitation, become an owner
IDOR on webhook URLStore an attacker webhook on victim’s account → exfil internal calls
IDOR + SSRF target configModify another tenant’s outbound webhook to an SSRF target

The rule of thumb: when a finding feels “meh,” ask what the output of this endpoint normally drives downstream. The downstream consumer is usually the real payout.


16. Detection Methodology with Autorize

Autorize is the canonical workflow. The goal: every request the app sends while you’re logged in as User A is automatically replayed as User B and as unauthenticated — and the response is diffed.

Setup

  1. Create two accounts: A (attacker) and B (victim). Use self-registration where allowed.
  2. Log in as B in a normal browser, log in as A in a separate browsing profile (or incognito).
  3. In Burp → Extender → BApp Store, install Autorize.
  4. Grab B’s session cookies and/or Authorization: Bearer token from B’s session.
  5. Paste them into Autorize’s config tab as the “low-priv” identity.
  6. Also add an empty entry for the unauthenticated user.
  7. Enable Autorize.
  8. Browse the app as A. Click everything. Load every page. Use every feature.

Autorize replays each request with B’s cookies and with no cookies, and classifies each triple:

A responseB responseUnauth responseVerdict
200200401IDOR (horizontal)
200200200IDOR (unauth — critical)
200403401Properly enforced
200302→login302→loginProperly enforced
200200 (different body)401Likely IDOR — diff required

Triage rules

  • Don’t trust status code alone. A 200 with an “access denied” JSON body is still a blocked request. Configure Autorize with a “Detector” string that marks blocked responses (e.g. "error":"unauthorized") to filter out false positives.
  • Response length matters. Same status, different length often means real data leakage.
  • Client-side 403s are a lie. Some apps return 200 and the frontend draws the error screen. Read the actual body.
  • Retry with headers stripped. Some 403s come from CSRF token checks or content-type enforcement, not authorization. Minimize the request.

Alternatives & supplements

  • Authz — older plugin, similar to Autorize but simpler config
  • AutoRepeater — lets you define rewrite rules and auto-replay in the background
  • AuthMatrix — role matrix testing across N roles; better for enterprise apps with many personas
  • Burp’s “Request in browser in other session” — manual equivalent
  • Two browsers side-by-side — always useful as the last sanity check before submitting

Manual diff workflow

When Autorize isn’t practical (heavy CSRF tokens, signed requests, state-dependent flows):

  1. Intercept request as A in Repeater.
  2. Duplicate to a second Repeater tab.
  3. Swap in B’s cookies/token.
  4. Swap in B’s CSRF token (grab from B’s last page load).
  5. Send. Compare with Burp Comparer (“Words” mode highlights diffs).
  6. Do the same with auth removed entirely.
  7. Finally, replay B’s request with A’s credentials — check the reverse direction too.

17. Tools & Automation

Manual / semi-automated

ToolPurpose
Burp Suite ProProxy, Repeater, Intruder, Comparer, Logger
AutorizeAutomatic cross-session replay and diffing
Authz / AuthMatrix / AutoRepeaterAlternative auth-testing plugins
ParamalyzerTracks parameters observed per host, highlights reuse candidates
Param MinerDiscovers hidden parameters and headers
Logger++High-volume request logging with filtering
Turbo IntruderHigh-throughput attack scripting (Python)
Request SmugglerDetects CL.TE/TE.CL for chain candidates

Automation / enumeration

ToolPurpose
ffufID brute forcing: ffuf -u https://t/api/orders/FUZZ -w ids.txt -fc 403,404
IntruderSniper mode on the ID parameter, range payloads
ArjunHidden parameter discovery
kiterunnerAPI route discovery with wordlists tuned for OpenAPI patterns
gau / waybackurls / katanaHistorical URL harvesting for leaked IDs
CeWLApp-specific wordlist generation
jwt_toolJWT manipulation for identity swap
Postman / Insomnia / BrunoSwagger/OpenAPI replay and mass testing
mitmproxyMobile app interception when Burp CA is awkward

Custom scripting

Quick Python replay loop for known victim IDs:

import requests

BASE = "https://api.example.com"
TOKEN_A = "attacker_token"
VICTIM_IDS = ["id_b_01","id_b_02","id_b_03"]

h = {"Authorization": f"Bearer {TOKEN_A}"}
for vid in VICTIM_IDS:
    r = requests.get(f"{BASE}/api/orders/{vid}", headers=h)
    if r.status_code == 200 and len(r.text) > 100:
        print(f"IDOR HIT: {vid} -> {r.status_code} {len(r.text)}B")

The mark of a good IDOR script is comparing against a known-403 baseline so you filter noise.


18. Impact & Severity Mapping

Bugcrowd VRT labels IDOR as “varies depending on impact.” Here’s the practical mapping.

ScenarioSeverityNotes
Read own old dataInformationalNot IDOR
Read another user’s non-sensitive data (display name)P4 / Low
Read another user’s PII (email, address)P3 / Medium
Read another user’s private messages / ordersP2 / HighPII + intent exposure
Modify another user’s profile or non-critical dataP2 / HighIntegrity impact
Delete another user’s dataP2 / HighIntegrity + availability
Read another user’s payment methods / stored cardsP1 / CriticalFinancial data
Account takeover via IDOR (password reset, email change, 2FA)P1 / Critical
Admin access via IDOR or BFLAP1 / CriticalPrivilege escalation
Cross-tenant read in SaaSP1 / CriticalRegulatory blast radius
Cross-tenant write / config takeover (Flowise SSO pattern)P1 / Critical
Unauthenticated IDOR at any level+1 severity vs. authenticatedMass scrapable

Multipliers that boost severity:

  • Enumerability — sequential IDs enable mass harvest (scale)
  • Automation-friendly — no CAPTCHA, no rate limit
  • No logging — app doesn’t alert on anomalous cross-user access
  • Regulatory data — HIPAA, PCI, GDPR-covered fields
  • No account required — unauthenticated access
  • Permanent state change — deletion without soft-delete

19. Prevention & Secure Design

Principle

Never trust the reference. Always authorize the operation against the current identity and the target object.

Concrete patterns

1. Scope queries to the current user at the ORM level.

Ruby on Rails:

# vulnerable
@project = Project.find(params[:id])

# secure
@project = current_user.projects.find(params[:id])

Django:

# vulnerable
Order.objects.get(pk=pk)

# secure
Order.objects.get(pk=pk, user=request.user)

Go (GORM):

// vulnerable
db.First(&order, id)

// secure
db.Where("user_id = ?", currentUser.ID).First(&order, id)

This is the single highest-ROI defense. It makes IDOR structurally impossible for that query path: a mismatched ID returns ActiveRecord::RecordNotFound / 404, not data.

2. Architect the base model with ownership.

The Nginx UI CVE happened because the base Model struct had no user_id column. Bake ownership into the foundation: every resource model inherits a tenancy/ownership field, every repository helper joins on it by default. Make the unsafe path the hard one to write.

3. Centralized authorization, not ad-hoc checks.

Inline if (user.owns(obj)) checks scatter, rot, and get forgotten on new endpoints. Prefer:

  • Policy objects (Pundit in Rails, cancan, casbin)
  • Middleware that resolves the object and runs authorize!(:read, obj) before the handler runs
  • Attribute-based access control (ABAC) with a single policy engine
  • Declarative rules (OPA / Rego) for multi-service consistency

4. Verify at every layer — UI hiding is not security.

Hiding the admin button client-side is UX, not access control. The handler must re-check.

5. Use unpredictable identifiers as defense in depth.

UUIDv4, ULIDs, or random strings make enumeration expensive. But never ship UUIDs as the only defense — access checks are still mandatory. OWASP explicitly warns against treating identifier complexity as a substitute for authorization.

6. Don’t encrypt IDs as the fix.

Encrypted IDs are hard to do right, leak via padding/length oracles, and the underlying auth gap remains. Add a real auth check instead.

7. Prefer indirect references.

Pass a session-bound token that maps server-side to the actual object. The mapping table lives only for the user’s session. The URL never contains the direct reference. OWASP’s classic IDOR recommendation.

8. Server-side session binding for multi-step flows.

Wizards that carry state across steps: store the state in the session, not in hidden form fields, so the user can’t tamper between steps.

9. Verb-level policy checks.

GET, PUT, DELETE on /resources/:id must each independently run the same ownership check. Framework annotations (@PreAuthorize in Spring, [Authorize] in ASP.NET with a resource-based handler) help, but audit them.

10. Avoid mass-assignment blindness.

Use allowlists (strong params in Rails, DTOs in Java/C#, Zod schemas in Node). Never spread req.body into an ORM model.

11. Logging, monitoring, audit.

Log every cross-user access attempt. Alert on anomalies (user suddenly reading from 50 distinct other-user IDs in a minute). Post-Flowise, audit SSO/config change events to a separate log with alerting.

12. Test access control in CI.

Write tests that hit every endpoint as (a) owner, (b) other user, (c) unauthenticated, and assert 403/404 for the latter two. Snapshot the authorization matrix and fail PRs that drop coverage.

Static analysis hooks

  • Grep for findBy(Id|Pk)(...) without a user_id/tenant_id clause
  • Grep for handlers that read body.userId, body.organizationId, body.accountId and trace whether they’re compared against request.user before use
  • CodeQL query: taint from req.body.*Id to db.update() without an intervening call into an authorization helper

20. Testing Checklist

Copy this into your test plan for any new target.

Preparation

  • Register two accounts (A and B). Same role by default.
  • Register a third account in a different tenant/org if multi-tenant.
  • Map all roles (anonymous, user, premium, admin, support, readonly).
  • Log in to each role in a separate browser profile.
  • Install and configure Autorize with all identity sets.
  • Grab Swagger/OpenAPI/GraphQL introspection if available.

Discovery

  • Walk every feature as A. Log every request in Burp.
  • Walk every feature as B. Note every object ID that appears.
  • Harvest IDs from JS bundles, source maps, HTML, API responses, emails.
  • Check robots.txt, sitemap.xml, .well-known/, Wayback Machine.
  • List every parameter across every endpoint (Paramalyzer).

Core IDOR tests

  • For each request carrying an ID, replay as B and as unauthenticated.
  • Increment/decrement numeric IDs. Try 0, 1, -1, leading zeros, large ints.
  • For UUIDs, try B’s UUID harvested from step above.
  • Test me / self / current → numeric/UUID swaps.
  • Add user_id parameters to endpoints that didn’t have one.
  • Rename parameters (album_idaccount_id) across endpoints.
  • Supply duplicate parameters (HPP).
  • Wrap IDs in arrays: {"id":[1234]}, {"id":[1234,1235]}.
  • Try string, float, negative, wildcard, null byte, boolean variations.

Method & format

  • Try every verb on every endpoint (GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS).
  • Try X-HTTP-Method-Override: PUT.
  • Switch Content-Type between JSON/XML/form/multipart/text.
  • Append .json, .xml, .xlsx, .config to resource URLs.

Path & version

  • Try v1, v2, beta, internal, legacy version prefixes.
  • Fuzz sibling endpoints under known resource paths.
  • Test trailing slash, case variations, URL encoding.
  • Check hidden Swagger, GraphQL introspection, WSDL.

Mass assignment

  • GET the resource, copy the response into a PUT, resubmit unchanged.
  • Add role, isAdmin, verified, balance, ownerId, organizationId.
  • Try to set password, passwordHash, 2faSecret directly.
  • Try to set createdAt, updatedAt, deleted.

Headers

  • Fuzz X-User-Id, X-UID, X-Account-Id, X-Customer-Id, X-Forwarded-User.
  • Fuzz X-Original-URL, X-Rewrite-URL for routing bypass.
  • Strip Authorization and retry — unauthenticated access.
  • Swap Referer / Origin to see if it gates anything.

Graph / WS / non-REST

  • Run GraphQL introspection.
  • Test every Query.<name>(id:) with a victim ID.
  • Test every Mutation.* with a victim ID.
  • Try alias batching: one request, many IDs.
  • Intercept WebSocket frames, swap IDs in subscribe/join actions.

Second-order & blind

  • Create scheduled/async jobs with victim ID in metadata.
  • Check email, SMS, push for side-channel confirmation.
  • Log into victim account and verify state actually changed.

Chain opportunities

  • Can IDOR on password reset → ATO?
  • Can IDOR + self-XSS → stored XSS?
  • Can IDOR + mass assignment → role escalation?
  • Does request smuggling exist? If yes, chain with any low-impact IDOR.
  • Can you pivot to webhook/SSRF targets via config IDOR?

Reporting

  • Two distinct accounts, clearly labeled (A = attacker, B = victim).
  • Full reproduction steps including how the victim ID was obtained.
  • Raw request/response pairs, not screenshots of JSON.
  • Impact statement: at scale, across tenants, PII exposed.
  • Remediation recommendation.

21. Report Writing

A strong IDOR report answers three questions in the first five lines:

  1. What can I access that I shouldn’t?
  2. How do I access it? (Minimal reproduction.)
  3. Why does this matter at scale?

Title formula

IDOR in <endpoint> allows <action> on any <resource> owned by <other user|org|tenant>

Examples:

  • IDOR in PUT /api/v1/loginmethod allows any user to overwrite SSO config of any organization
  • IDOR in GraphQL UpdateSound allows any user to rename any soundtrack in the library
  • Unauthenticated IDOR in /api/orders/{id} allows anyone to read any user's order history

Body template

Summary:
An authenticated user on the Free plan can overwrite the SSO configuration
of any other organization by supplying an arbitrary organizationId in the
body of PUT /api/v1/loginmethod. The endpoint requires authentication but
does not verify that the caller belongs to the target organization.

Steps to Reproduce:
1. Register two free-tier accounts in two different organizations: Attacker
   (org_A) and Victim (org_B).
2. As Attacker, obtain org_B's organizationId from <source> (e.g. API
   response, enumeration, profile page).
3. As Attacker, send the following request using Attacker's JWT:

   PUT /api/v1/loginmethod HTTP/1.1
   Host: cloud.example.com
   Cookie: token=<attacker_jwt>
   Content-Type: application/json

   {"organizationId":"<org_B_id>",
    "userId":"<org_B_admin_id>",
    "providers":[{"providerName":"google",
                  "config":{"clientID":"ATTACKER_OAUTH",
                            "clientSecret":"ATTACKER_SECRET"},
                  "status":"enable"}]}

4. Server responds 200 OK. Victim org's SSO now points at Attacker-controlled
   OAuth app.
5. Log in to Victim's app as an org_B employee — authentication flows through
   Attacker's OAuth and returns Attacker-captured identity.

Impact:
- Complete account takeover of every employee in any targeted organization
  who logs in via SSO.
- Privilege escalation: Free-tier users can enable Enterprise-only SSO
  features without a license.
- Regulatory: uncontrolled cross-tenant identity bridging; every affected
  org must invalidate sessions and rotate OAuth credentials.

Remediation:
- Add ownership check: require request.user.organizationId === body.organizationId.
- Enforce role check: only org admins may modify loginmethod.
- Add license/tier check: reject enterprise-only provider changes on free plans.
- Audit log every SSO configuration change with alerting.

Common report-killing mistakes

  1. Testing with only one account (can’t prove cross-user impact).
  2. Stopping at 403 — always retry with headers stripped, methods swapped, Content-Type changed.
  3. Missing the impact statement. “I accessed data” is not impact. Scale + PII + regulatory framing is.
  4. Screenshotting JSON instead of pasting the raw request/response.
  5. Confusing IDOR with “I can see my own old data” (not IDOR).
  6. Not clarifying horizontal vs vertical vs unauthenticated.
  7. Reporting duplicate IDs that were intentionally public (product pages, press releases).
  8. Automated scanning in programs that prohibit it — read the rules.

Calibrating severity for triage

  • Always lead with unauthenticated if applicable — that’s the headline.
  • Always note scale — “any user” beats “one user.”
  • If cross-tenant, say “cross-tenant” explicitly — SaaS programs treat this as its own class.
  • If chained, explain the chain concisely and score for the chained impact.
  • Call out enumerability — sequential IDs plus no rate limit equals mass scrape.

Appendix: Quick-Reference Payload Cheatsheet

ID mutations to try in order

123              your own ID (baseline)
124              +1
122              -1
1                lowest
0                zero
-1               negative
00000123         leading zeros
123.0            float
1.23e2           scientific
[123]            array wrap
[123,124]        array multi
"123,124"        string delimited
123%00           null byte
*                wildcard
%                SQL wildcard
true             boolean
null             null
me, self, current, admin     aliases

Headers to fuzz for identity override

X-User-Id: <victim>
X-UID: <victim>
X-User: <victim_email>
X-Account-Id: <victim>
X-Customer-Id: <victim>
X-Tenant-Id: <victim>
X-Forwarded-User: <victim>
X-Forwarded-Email: <victim>
X-Original-User: <victim>
X-Auth-User: <victim>
X-Remote-User: <victim>
X-Impersonate: <victim>

Parameters to add on create/update

id, uid, user_id, userId, owner, ownerId, account_id, accountId,
organizationId, orgId, tenantId, role, isAdmin, admin, permissions,
verified, emailVerified, status, tier, plan, balance, credit,
created_at, updated_at, deleted, deletedAt, password, passwordHash

Endpoints that commonly fail on ownership

/api/v1/loginmethod              <-- SSO / OAuth config
/api/users/{id}/email            <-- email change
/api/users/{id}/password         <-- password set
/api/users/{id}/2fa              <-- 2FA enroll/disable
/api/accounts/{id}/billing       <-- payment methods
/api/orgs/{id}/members           <-- team management
/api/orgs/{id}/invites           <-- pending invitations
/api/webhooks                    <-- stored callback URLs
/api/exports                     <-- scheduled exports
/api/files/{id}                  <-- file download
/api/sessions/{id}               <-- session management
/api/api-keys/{id}               <-- API token management

Sources

This guide synthesizes the following clipped research articles:

  1. Aon Cyber Labs — Finding More IDORs: Tips and Tricks
  2. Intigriti — IDOR: A Complete Guide to Exploiting Advanced IDOR Vulnerabilities
  3. dev.to / Kai — How to Find IDOR Vulnerabilities: The Bug Bounty Hunter’s Practical Guide
  4. Hadrian — Insecure Direct Object Reference (IDOR): A Deep Dive
  5. OWASP Cheat Sheet Series — Insecure Direct Object Reference Prevention
  6. OWASP Foundation — Insecure Direct Object Reference
  7. MDN / Security — Insecure Direct Object Reference
  8. Varonis — What is IDOR?
  9. Bugcrowd — How-To Find IDOR Vulnerabilities for Large Bounty Rewards
  10. DailyCVE / Flowise — IDOR & Business Logic Flaw (CVE-2025)
  11. hipotermia.pw — HTTP Request Smuggling + IDOR chain writeup
  12. thehackerwire — Nginx UI IDOR (CVE-2026-33030)
  13. Appsecure — Reddit Bug Bounty: Exploiting IDOR in Dubsmash UpdateSound API
  14. Insecure Direct Object References (IDOR) overview
  15. What is an example of a real bug bounty report where IDOR was used
  16. Footstep Ninja blog series on IDOR hunting (multiple entries)

Defensive reference only. Use this guide to test systems you are authorized to test, harden designs you own, and score findings consistently during triage.