Mastering IDOR: Testing Broken Access Control — ioSENTRIX blog hero with two user records and an open padlock.
TABLE Of CONTENTS

Mastering IDOR: Finding Broken Access Control Behind Predictable IDs and UUIDs

Salman Khan
2026-09-10
9
min read
About these case studies. The techniques below are drawn from real assessment and research work, fully anonymized. Every screenshot is illustrative and mocked — the hostnames (target.com), identifiers, emails, and any record fields shown are AI-generated and fabricated for demonstration; they do not represent any real user, record, or client system. Where a technique depends on framework-specific behavior, that behavior is described from public documentation, not from any customer’s configuration.

An attacker changes one number in a request — user_id=212 becomes 213 — and your application hands back someone else’s profile. No exploit chain, no memory corruption, no clever payload. Just a request the server should have refused and didn’t.

That is Insecure Direct Object Reference (IDOR), and it is still one of the most common and most impactful findings we report. It is not a niche bug. It sits inside Broken Access Control, which OWASP ranks #1 in both the 2021 and the current 2025 editions of the Top 10, and which the OWASP API Security Top 10 calls out at the API layer as Broken Object Level Authorization (BOLA, API1:2023). At the code level it maps cleanly to CWE-639: Authorization Bypass Through User-Controlled Key.

The reason it persists is worth stating plainly, because it shapes how you should test for it: IDOR is caused by missing authorization, not by weak identifiers. The identifier is just the key the attacker turns. Whether that key is a sequential integer or a 128-bit UUID changes how hard the resource is to discover — it does nothing to change whether the server authorizes the request. An application that leans on unguessable IDs instead of server-side authorization checks is not secure. It is undiscovered.

This post walks through how we test for IDOR across the two cases that matter in practice — predictable identifiers and unpredictable ones — with anonymized examples from real work, and closes with what a durable fix actually looks like.

Know your identifiers first

Before you test, understand what the application uses to reference objects. It drives your whole approach.

Predictable identifiers are sequential or trivially derivable: 101, 102, 103, an auto-increment primary key, a short order number. They are easy to enumerate — increment the value, watch the response. When authorization is missing, discovery is almost free.

Unpredictable identifiers are UUIDs, hashes, random strings, or opaque tokens — 8f4c9a7d-4e7b-4c13-9f1d-5f7a0f3d6b12, 550e8400-e29b-41d4-a716-446655440000, or a signed token like eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…. A random version-4 UUID carries roughly 122 bits of entropy, so brute-forcing one is not a realistic path. That leads a lot of teams to treat these identifiers as an access-control boundary. They are not. The identifier being hard to guess only moves the attacker’s problem from guessing the ID to obtaining it — and applications leak valid identifiers constantly through API responses, shared links, exports, logs, notifications, historical URLs, and client-side JavaScript.

Predictable sequential IDs versus unpredictable UUIDs and tokens used to reference application objects.

The rule that follows from this is the one to carry into every test: security must never depend on how hard an identifier is to guess. It must depend on whether the server authorizes the request that carries it.

Part 1 — IDOR with predictable identifiers

The mechanics

When an application references resources by a predictable value and skips the server-side ownership check, an attacker changes the value and reaches another user’s data. Depending on the endpoint, that is unauthorized read, unauthorized modification, or deletion.

GET /profile?user_id=212        →  your profile
GET /profile?user_id=213        →  someone else’s profile   ← IDOR

How to test it properly

Predictable IDOR is simple to find and easy to miss — not because it is subtle, but because testers don’t test every place an object is referenced. Object identifiers do not only live in the URL query string. They appear in path segments, POST bodies, JSON fields, GraphQL variables, cookies, custom headers, and hidden form fields. Map the whole application and inspect every request that carries user-controlled data, wherever that data sits in the request.

The method itself is disciplined, not clever:

  1. Use at least two accounts. Create User A and User B (ideally at different privilege levels).
  2. Capture A’s requests in Burp Suite Proxy and note every object identifier.
  3. Replay with B’s identifiers in Repeater — swap A’s IDs for B’s, keeping A’s session — and check whether the server enforces ownership.
  4. Confirm both directions, and use the second account to eliminate false positives (a resource that looks “leaked” may simply be public).

For breadth, Burp Intruder enumerates a range of IDs quickly, and the Autorize extension automates the core comparison — it replays every request under a low-privilege session and flags where access was not correctly enforced. Autorize is the single biggest time-saver for authorization testing at scale, and it belongs in the workflow for any application with more than a handful of endpoints.

IDOR testing methodology for predictable identifiers: two accounts, intercept, swap the ID, observe, confirm.

Case study #1 — A serialized payload hiding the real identifier

During recon on one application, almost every operation flowed through a single backend endpoint: /_serverFn/…. The request headers gave the framework away immediately — x-tsr-serverfn: true, x-tss-serialized: true, and Accept: application/x-tss-framed. This is TanStack Start, whose server functions serialize their arguments into a single structured payload (via the seroval serializer) rather than exposing conventional named parameters.

Decoded, the payload was a nested object where each node is tagged by type and index rather than by readable field names:

{
 "t": { "t": 10, "i": 0, "p": {
   "k": ["data"],
   "v": [{ "t": 10, "i": 1, "p": {
     "k": ["portalUserId"],
     "v": [{ "t": 0, "s": 11 }]
   }}]
 }}
}

The point is not the exact schema — it is that the user-controlled identifier (portalUserId) was buried inside a serialized structure that looked, at first glance, like framework noise. Changing that value and replaying the request returned another user’s portal data. From there the same pattern repeated across function after function: the backend accepted client-supplied identifiers and never verified ownership, producing a critical, site-wide IDOR affecting a wide range of user resources.

A TanStack Start server-function request whose seroval-serialized payload hides a user-controlled portalUserId (illustrative, mocked data).

Takeaway: Never skip encoded or serialized requests. Modern frameworks — TanStack Start, Next.js server actions, tRPC, GraphQL — routinely pack identifiers into a single opaque-looking payload. Decode it, find the object reference, and test it exactly like a URL parameter, because to the authorization layer it is one.

Case study #2 — Predictable order IDs behind a “send my invoice” feature

An e-commerce application let users email their own purchase invoice. The request carried an order identifier that looked unpredictable because of its prefix:

orderId=COMP-21234

My first instinct — swapping in random values — returned nothing, so I moved on. Weeks later, reviewing notes, I saw what I’d missed: the prefix was constant and only the numeric tail changed between orders. That reframes the whole identifier. COMP- is decoration; the real key is a short, sequential integer.

Sent to Intruder with the numeric portion as the payload position, the enumeration produced HTTP 200s with markedly larger response sizes — the tell that an invoice was generated and dispatched — and other customers’ invoices began arriving in my inbox. The backend generated and emailed an invoice on the strength of the supplied order ID alone, never checking that the order belonged to the requester. The issue was responsibly disclosed and rewarded through the program.

This is BOLA plus a second gap worth naming: an endpoint that will happily iterate through the full ID space with no rate limiting or lockout also falls under Unrestricted Resource Consumption (API4:2023). The authorization failure is the root cause; the missing rate limit is what makes it trivially scalable.

Enumerating the numeric portion of a predictable order ID in Burp Intruder to retrieve other users' invoices (illustrative, mocked data).

Takeaway: A prefix, a hash-looking segment, or a formatted string is not evidence of an unpredictable identifier. Isolate the part that actually varies between objects and test that. And re-test functionality you dismissed earlier — the second look catches what the first pass rationalized away.

Part 2 — IDOR with unpredictable identifiers (UUIDs and tokens)

Unpredictable identifiers do not eliminate IDOR. They change the attacker’s job from guessing the identifier to discovering it. The entire game becomes: where does the application hand out a valid identifier belonging to someone else?

Unpredictable identifiers such as UUIDs and tokens hide resources but do not authorize requests.

Single-role applications

When every user has the same role and the same functionality, there is no higher-privileged view leaking lower-privileged objects, which makes identifiers harder to source. Look outside the live application:

  • Historical and archived URLs
  • Publicly accessible or shared resources and links
  • Search-engine caches
  • API responses and exported files
  • Email notifications
  • Client-side JavaScript and source maps

Archived URLs are a reliably productive source. waybackurls pulls a domain’s historical URLs from the Internet Archive:

waybackurls target.com

Identifiers captured in old URLs frequently remain valid long after those URLs disappear from the live site — because the object still exists and the server still won’t check who’s asking.

waybackurls output exposing historical URLs that still contain valid resource identifiers (illustrative, mocked data).

Case study: On one engagement every resource identifier was a long random string, so brute force was off the table. Instead of attacking the keyspace, I mined historical URLs and archived API responses and recovered a set of valid resource identifiers. Supplying them to the application’s API returned resources belonging to other users — authorization was simply absent. UUIDs did not prevent the IDOR; they relocated it from guessing to harvesting.

Multi-role applications

Applications with several roles — Administrator, Manager, Employee, Merchant, Vendor, Support, Organization Owner — are the richest hunting ground for UUID-based IDOR, because roles constantly exchange identifiers through legitimate workflows. Watch how identifiers move between roles in user management, org-member lists, audit logs, reports, notifications, approval flows, admin dashboards, and shared API responses. Test with multiple accounts at different privilege levels and track where an identifier that should be scoped to one role becomes visible to another.

Case study — a complex identifier leaked by a neighboring endpoint. A user-verification endpoint accepted a 24-character random selfUserId. On its own it looked untouchable. But a separate endpoint that returned public project details exposed the project owner’s selfUserId in its response. With that value in hand, I replayed the verification request — which allowed updating a user’s verification details — against the victim’s identifier. Because the server trusted the client-supplied ID and never verified ownership, another user’s verification record (name, phone, address, country, and uploaded government-issued ID documents) could be read and modified.

A public endpoint leaking another user's selfUserId, replayed against a verification endpoint that never checks ownership (illustrative, mocked data).

Takeaway: A complex identifier is not an authorization control. The moment any endpoint discloses it, the resource it points to is only as protected as the next endpoint’s ownership check — and here there wasn’t one.

Case study — privilege escalation via a leaked admin identifier (BFLA). This one is worth distinguishing carefully. Two accounts were in scope: an Administrator who could delete users, and a View-Only user with read-only access. The delete request accepted a complex user identifier that looked unguessable — but the View-Only user’s own profile response leaked the Administrator’s internal identifier in a createdBy field. Replaying the admin-only delete request from the View-Only session, with the leaked admin identifier substituted in, succeeded: a low-privilege account performed an administrator-only action.

Strictly, this is two failures chained. The identifier leak is object-level (BOLA/CWE-639). But a View-Only user reaching a delete function at all is Broken Function Level Authorization (BFLA, API5:2023) — the server authorized neither the object nor the operation. Naming both matters, because fixing only the identifier leak would leave the more dangerous gap — an unprivileged user invoking a privileged function — wide open.

A view-only user performing an admin-only delete by substituting a leaked administrator UUID: BOLA chained with BFLA (illustrative, mocked data).

Takeaway: UUIDs hide resources; they never replace authorization. When you find a leaked privileged identifier, check whether the sensitive function is also reachable by the wrong role. The chain is usually worse than either link alone.

What a real fix looks like

Every case above has the same root cause and, therefore, the same class of fix: authorize on the server, on every request, against the authenticated session — never against a value the client supplied. That is the control we test for, and it is worth being specific about what “good” means, because a paper policy that says “we check authorization” is not the same as a control that holds under a replayed request.

  • Enforce object-level ownership server-side, per request. Derive the acting user from the session or validated token, then confirm that user is authorized for the specific object and the specific operation before doing any work. Never infer authorization from the mere presence of an identifier in the request.
  • Default to deny. New endpoints and new object types should require an explicit access decision, not inherit implicit access. Most IDORs we find are on endpoints nobody remembered to gate.
  • Don’t rely on unpredictability as a control. Unguessable IDs (and avoiding sequential public keys) are good defense-in-depth against enumeration, per the OWASP IDOR Prevention Cheat Sheet — but they are a supplement to authorization, never a substitute.
  • Add rate limiting and monitoring on object-referencing endpoints so that even a partial gap can’t be harvested at scale, and so enumeration attempts are visible.
  • Test it, don’t assert it. Regression-test authorization with a second, lower-privileged account in CI, and validate the control adversarially — the way an attacker actually exercises it — not by reading the code and trusting it.

That last point is the ioSENTRIX position, and it is the whole reason IDOR keeps reaching production: a control that exists in the codebase is not the same as a control that holds when a real request tries to bypass it. The only way to know which one you have is to prove it.

Frequently asked questions

What is an IDOR vulnerability?
Insecure Direct Object Reference is an access-control flaw where an application exposes a reference to an internal object — a user ID, order ID, or document ID — and fails to verify that the requesting user is authorized to access that object. Changing the identifier grants access to another user’s resource. It is a form of Broken Access Control (OWASP A01) and, at the API layer, of Broken Object Level Authorization (BOLA, API1:2023).

Do UUIDs prevent IDOR?
No. UUIDs make a resource harder to discover by brute force, but they do not authorize requests. If an attacker obtains a valid UUID from an API response, a shared link, an archived URL, or a leak from another endpoint, the resource is exposed unless the server independently verifies ownership on every request.

How do you test for IDOR?
Map every place the application references an object — URLs, path segments, POST bodies, JSON and GraphQL fields, cookies, headers, and serialized payloads. Using at least two accounts at different privilege levels, capture requests from one account and replay them with the other account’s identifiers, checking whether the server enforces ownership. Burp Suite’s Repeater, Intruder, and the Autorize extension are the core toolset.

What is the difference between BOLA and BFLA?
BOLA (Broken Object Level Authorization) is accessing an object you shouldn’t — another user’s record via its identifier. BFLA (Broken Function Level Authorization) is invoking a function you shouldn’t — a low-privilege user calling an admin-only operation. They frequently chain: one endpoint leaks a privileged identifier (BOLA) and another lets the wrong role use it (BFLA).

What is the correct fix for IDOR?
Enforce authorization on the server for every request, derived from the authenticated session rather than from client-supplied values, checking both the specific object and the specific operation. Default to deny, add rate limiting and monitoring, and treat unguessable identifiers as defense-in-depth against enumeration — not as an access-control boundary.

ioSENTRIX Can Help

ioSENTRIX is a CREST-accredited, ISO/IEC 27001 certified penetration testing firm. Broken access control is the most common serious finding we report, and IDOR is its most persistent form precisely because it hides behind identifiers that look safe. Our web and API penetration testing exercises every object reference and every privileged function with the same discipline shown above — multiple accounts, adversarial replay, and evidence — so you learn whether your authorization actually holds, not just whether the code says it should. If you’re shipping APIs, multi-tenant features, or role-based access, we can help you prove those controls work before an attacker tests them for you.

Keep reading

  • API Security Testing: Beyond the Scanner
  • Broken Access Control: The Bug Scanners Miss
  • Why Authorization Belongs in Your CI Pipeline
  • Threat Modeling Multi-Tenant Applications
  • Prove, Don’t Assert: What Adversarial Testing Actually Buys You
#
OWASPTop10
#
Penetration Testing
Contact us

Similar Blogs

View All