
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.
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.

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.
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
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:
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.

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.

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.
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.

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.
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?

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:
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.

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.
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.

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.

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.
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.
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.
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 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.