Playbooks
The IDOR checklist: how to test for insecure direct object references
A step-by-step IDOR checklist for 2026: find every place an object ID travels, replay it from a second account, then fix it with a server-side ownership check and lock the fix in with a test.
Cyberlop Labs · Updated · 15 min read
IDOR (insecure direct object reference) is a flaw where an app returns or changes a record because the request names its ID, without checking the caller owns it. To test, use two accounts, replay each ID from the other account on every operation, then fix it with a server-side ownership check and a regression test.
- IDOR is a missing ownership check on an object the client names; the fix is a server-side check, not a harder-to-guess ID.
- Test with two accounts, plus a second tenant and a logged-out session, and replay each ID on read, update, delete, create and export.
- IDs hide in paths, queries, bodies, headers, cookies, GraphQL arguments, file names, exports and mobile routes, so list them all first.
- UUIDs and encoded IDs slow guessing but do not replace a check: learn a real ID from a list or link, then replay it.
- Scope the query to the owner from the session, deny by default, and add a two-account regression test for each route.
- Prove the fix by replaying the exact original request against the new code.
An IDOR checklist is a repeatable set of checks for one question: when a signed-in account changes an identifier in a request, does the server still return or change data that belongs to someone else? To test it, use two accounts, replay each request that carries an ID with the other account's ID, and compare the responses. If the second account gets a 200 and the first account's data, you have an IDOR.
The hard part is finding every place an identifier hides, then recording each result so a developer can fix it and a tester can prove the fix.
What is IDOR?
Insecure direct object reference means the application uses an identifier supplied by the client to fetch or change an object, and never checks that the caller is allowed to touch that object. The OWASP IDOR Prevention Cheat Sheet names three ingredients: an object, a reference to it (an ID, UUID, token or slug) and a missing object-level authorisation check. Remove any one and the bug disappears.
The classic case is /invoices/1042 changed to /invoices/1043. The user is authenticated, so the request passes the login check, but nothing asks whether invoice 1043 is theirs. PortSwigger also shows the file version: /static/12144.txt changed to another number.
IDOR is not a separate family from other access-control bugs. It sits inside broken access control, which is number one (A01) in the OWASP Top 10:2025 and maps to CWE-639, Authorization Bypass Through User-Controlled Key. In API terms the same flaw is called BOLA; the BOLA post covers the naming difference and the code-review angle.
How common is IDOR?
IDOR sits inside the most common class of web flaw. In the OWASP Top 10:2025, A01 Broken Access Control is still number one, with an average incidence rate of 3.74%, a maximum of 20.15%, 40 mapped CWEs and 1,839,701 occurrences in the contributed data. CWE-639, the IDOR weakness, is one of the 40.
The 2025 CWE Top 25 shows the same family from another angle. CWE-862 (Missing Authorization) ranks fourth with a score of 13.28, and CWE-639 ranks 24th with a score of 2.62.
It is also cheap to find. The OWASP API Security Top 10 (2023) rates API1, BOLA, as easy to exploit, widespread and easy to detect, with moderate technical impact. Older bounty data agrees: HackerOne reported in 2021 that IDOR made up 36% of vulnerability bounties in medical technology and 31% in professional services.
Is IDOR the same as BOLA?
Yes, in practice. IDOR is the older web name for the bug and BOLA is the OWASP API name for it; both map to CWE-639.
The three terms overlap. Pick the one your audience expects and keep the evidence the same.
- IDOR is the older web term. It describes the mechanism: a direct reference the client controls.
- BOLA is the name used by the OWASP API Security Top 10 (2023), where it is risk number one. It describes the missing check at object level.
- Horizontal privilege escalation describes the effect: one account reaching a peer's data. When the same trick reaches an admin function, it becomes vertical privilege escalation.
In a report, use whichever name the client's team searches for, then add the CWE and the OWASP reference so the finding maps cleanly to audits.
What do you need before you test for IDOR?
Most IDOR testing fails at setup, not at the request. You need accounts that own different objects, and you need to know which objects belong to whom.
- Create at least two accounts of the same role (call them A and B). This tests horizontal access.
- Add one account of a lower role and one of a higher role. This tests vertical access, such as a member reaching an admin object.
- If the product is multi-tenant, create two tenants with at least one account each. Cross-tenant IDOR is the version that ends up in a customer's security questionnaire. See tenant isolation.
- Have each account create real objects: an order, a file, a message, a saved card label, an invite. Write down every ID created, with its owner.
- Use a test environment with realistic data, not production, unless you have written permission and a plan for the data you will touch.
Keep a simple access plan as a table: object type, who owns it, who may read it, who may change it, who may delete it. Every row becomes a set of test cases. The access control testing checklist shows how to build that plan in more depth.
Where do object IDs hide in an application?
People who only look at the address bar miss most IDORs. Build the list of identifiers from the whole traffic log, not from the page you can see. Walk through the app with a proxy running and tag every value that looks like a reference.
In the request
The table lists the request locations to tag while you click through the app with a proxy running.
| Location | Example | What to try |
|---|---|---|
| Path segment | /api/orders/8841, /users/me/addresses/17 | Swap in account B's ID |
| Query string | ?account_id=, ?file=, ?invoice= | Swap, then send two values |
| JSON body | {"order_id": 8841}, nested objects, bulk arrays | Swap inside nested and bulk calls |
| Form and hidden field | A hidden user_id, as in the MDN IDOR page | Edit the field in developer tools |
| Header or cookie | X-Account-Id, X-Tenant, a customer-number cookie | Change the value, keep the session |
| Multipart upload | A form attaching a file to a record by ID | Attach to account B's record |
In the response and in quieter places
- Lists that return objects with IDs you did not own before, plus links to downloads, exports and signed URLs.
- Error messages, logs and email notifications that print or link to other IDs.
Features that get less review
- Background features: PDF exports, CSV downloads, report generators, email previews, "share" links, webhooks you can register.
- Search and autocomplete endpoints that return records before any access filter is applied.
- Old API versions and routes that the interface no longer calls but the server still serves. This is where a shadow API or zombie API hides.
How do you test for IDOR? The checklist, step by step
Run this loop for every request that carries an identifier. It is the core of IDOR testing and matches the OWASP Web Security Testing Guide (WSTG-ATHZ-04), which advises at least two users who own different objects and functions, so you can try one account's references from the other.
- Capture the request as account A, on an object A owns. Save the full request and response.
- Swap the identifier for one owned by account B. Keep A's session token. Send it.
- Compare. A
403or404with no data is a pass. A200with B's data is a finding. A200with an empty body needs a second look: some apps return the same shell page whether or not the object exists. - Test every verb, not just GET. Try PUT, PATCH, POST and DELETE on B's object. Reads often work the way they should while writes slip through.
- Test the same call with no session at all. If it still returns data, the problem is wider than IDOR: see unauthenticated access.
- Test a lower role against a higher role's object, and a user from tenant 1 against tenant 2.
- Test the child objects. Parent checks are often right and child checks are not: an order may be protected while
/orders/8841/items/3, its invoice PDF or its comments are open. - Test creation. When A creates a record, can the request set an owner or tenant field to B? That is a mass assignment issue that ends as IDOR.
- Record the outcome as a pass, a fail or "not applicable", with the request, the response and the two account names.
Which operations should you test on each object?
Run the swap on every operation an object supports. The table shows what to send and what a pass looks like.
| Operation | Send as the wrong account | Pass looks like |
|---|---|---|
| Read | GET the object, its list, count and search results | 403 or 404, no data in the body |
| Update | PUT, PATCH or POST new values | Rejected; stored value unchanged |
| Delete | DELETE the other account's object | Rejected; owner still has it |
| Create | Set an owner or parent ID that is not yours | Owner taken from the session |
| Export and download | Reuse export and file links with another session | Link refuses a non-owner or has expired |
| Admin and bulk | Call bulk, import and admin routes as a normal account | Role check fails before any data is touched |
Per-request IDOR test list
- Own object works as the owner (baseline).
- Other account's object, same role, read.
- Other account's object, same role, update.
- Other account's object, same role, delete.
- Other tenant's object, any verb.
- Higher-role object from a lower-role account.
- Same request with no session and with an expired session.
- Same object through a second route (export, download, search, old API version).
- Identifier moved between locations: path, query, body, header.
What do you try when the simple swap is blocked?
Developers often block the obvious change and miss the neighbours. These variations catch the second layer of mistakes.
Change the shape of the identifier
Try these variations
- Send two values.
?id=A_ID&id=B_IDcan pass a check on the first value and return the second. This is HTTP parameter pollution. - Send an array where a single value is expected:
{"id": [A_ID, B_ID]}. - Wrap the value in an object, or switch the content type between JSON, form and XML if the endpoint accepts more than one.
- Add wildcards or special values such as
*,0,-1or an empty value, and see whether the endpoint returns a list instead of one record. - Change the extension or format:
/orders/8841.jsonmay skip middleware that protects/orders/8841.
Change the route
Try these routes
- Try the same object through a different verb or path, for example a legacy
/v1/route next to/v3/. - Call internal-style routes the front end never uses, such as
/admin/orders/8841. That edges into forced browsing. - Replay the request with
mereplaced by an explicit ID, or an explicit ID replaced byme.
Change what the server trusts
- If the identity comes from a JWT claim, check that the server uses the signed claim and ignores a user ID in the body. The mitigation advice under CWE-639 comes down to one habit: authorise every single data access against the specific record being asked for.
- If a header such as
X-Tenantselects the tenant, try changing it. Tenant headers set by a gateway should never be accepted from the client.
Do UUIDs and encoded IDs stop IDOR?
No. UUIDs and encoded IDs make guessing harder, but only a server-side ownership check stops IDOR. The test changes from guessing an ID to learning one and replaying it.
Switching from numbers to UUIDs makes enumeration harder. It does not add an authorisation check. The OWASP cheat sheet agrees that complex identifiers are hard to guess, then insists the access control check still has to exist. The API Security Top 10 likewise lists random GUIDs beside a proper authorisation mechanism, never instead of one. See unguessable IDs.
So the test changes. You no longer guess the other account's ID. You obtain it, then use it.
| Identifier type | What to try | What a pass looks like |
|---|---|---|
| Sequential number | Replace with a neighbour or with account B's own ID. | Denial, no data from B. |
| UUID | Take B's UUID from a list, link, export or from B's own session and send it as A. | Denial, even though the ID is valid. |
| Base64 or hex wrapper | Decode, change the inner value, re-encode. | Server rejects or denies the changed value. |
| Hash of an email or name | Compute the hash for B's email and send it. | Denial; the hash is not treated as proof of ownership. |
| Signed or expiring link | Use it after expiry, from another account, and with one character changed. | Expiry enforced and signature checked. |
| Filename or path | Request B's file name directly, with and without a session. | Denial; files are not public by default. |
How to obtain an identifier you were not given
- Read it from an endpoint that lists or searches shared objects: team directories, comments, activity feeds, public profiles.
- Read it from an email, a share link, an export file or a webhook payload.
- Read it from an error message, a log view or the page source.
- Use account B's own ID, which you know because you created B. In a real test this is the strongest evidence, because nothing was guessed.
If a value looks random, check. Base64 around a number, a hex integer, an MD5 of an email address or a version 1 UUID (which embeds a timestamp) can all be predicted. If the app signs the value, change one character and confirm the server rejects it.
How do you test IDOR in REST and GraphQL APIs?
APIs expose the object model directly, which is why OWASP puts object-level authorisation first. The API security testing post walks through the full list; these are the IDOR-specific points.
- Use the OpenAPI file or a recorded session to list every route with an ID. Check them with a script, not by hand.
- Bulk and batch endpoints.
POST /orders/lookupwith a list of IDs may check the first and return all. Put one of your IDs first and one of B's second. - Nested routes.
/teams/5/members/9may check team 5 but not whether member 9 belongs to it. - Filters and expansions.
?include=owner,?fields=and?expand=can return related objects that were never authorised on their own. - GraphQL. Query the same node by ID through different fields. A resolver at the top may check ownership while a nested field does not. Test mutations as well as queries, and check batched operations.
- Pagination and search. A list endpoint that returns a page of other users' records is IDOR without any ID being changed.
How do you test IDOR in mobile apps, files and exports?
Mobile and file features often get a lighter review than the web interface, though they call the same back end.
- Mobile apps. Route the app's traffic through a proxy and run the same swap on every call. Apps often hold hard-coded API versions and older routes. Check deep links too: a link that opens
app://invoice/8841must still ask the server who is allowed to read it. - File downloads. Predictable filenames such as
/uploads/12144.pdfare the oldest IDOR. Check whether object storage URLs are public by default and whether signed links expire. - Multi-step flows. Check that step three re-checks the owner, instead of trusting an ID set at step one.
What should an IDOR finding record?
A finding that the developer can reproduce in one go gets fixed. A vague one gets argued over. Record:
- The exact request, with the session identified by account name and role, and the object ID used.
- The response, with the part that proves exposure highlighted (the other account's name, email or amount).
- Who owned the object, so the reader knows no guess was involved.
- The verbs that worked: read, update, delete.
- Impact in one line: what the data is, and how many records could be reached if the ID space is enumerable.
- A screenshot where a person can see it in the interface.
- In a white-box test, the file and line where the lookup skips the check.
Keep severity and confidence apart. A read-only exposure of low-sensitivity data and a delete on every customer's invoices are both IDOR, but they are different severities. The read a finding post explains the four-part finding, and severity versus confidence covers the split.
How do you fix IDOR?
Fix IDOR on the server, where the data is fetched: make the owner part of every lookup and deny by default. Hiding or randomising IDs is an extra layer, not the fix.
Fix patterns that hold up
Fixes that work put the check where the data is fetched. Fixes that fail hide the identifier and leave the lookup alone.
Check on the server, on every access
The ownership check must run for each read, update and delete. The IDOR cheat sheet points the same way: when you fetch an object by its key, search only among the objects this caller may use. An object outside that set simply does not exist as far as the caller is concerned.
Scope the query, not the response
Do not fetch the record and then decide whether to show it, if you can avoid it. Add the owner or tenant to the query itself. In databases that support it, row-level security enforces the same rule below the application, so a forgotten check in one route cannot expose data.
Take identity from the session
For "my" resources, do not accept a user ID from the client at all. Read it from the session or the validated token. Strip owner and tenant fields from create and update bodies, which also closes the mass assignment route.
Deny by default
New routes should refuse access until a rule says otherwise. The A01:2025 prevention advice includes deny by default for everything non-public, one shared access-control layer, and record ownership built into the data model. A shared helper or middleware that every handler must call is safer than a check copied into each handler.
Use unguessable IDs as an extra layer
Random identifiers reduce scraping and make enumeration noisy. Add them after the check exists. Do not rely on secrecy of the ID, because IDs leak through links, logs and lists.
Where the existence of an object is itself sensitive, return the same status for "not yours" and "not there".
How do you stop IDOR coming back?
IDOR returns when someone adds a route. The OWASP Authorization Testing Automation Cheat Sheet blames the usual cause: releases add or change features and nobody checks what that does to authorisation. The remedy is a test that fails when a check goes missing.
- For each route with an ID, write a test with two accounts. Account A creates the object. Account B requests it. Expect a denial and no data in the body.
- Run it for read, update and delete.
- Generate the list of routes from your router or OpenAPI file, so a new route without a test is flagged.
- Run the tests in CI/CD and fail the build on a missing denial. See security regression test.
- After a reported IDOR, replay the exact original request against the fix. This is a closed-loop retest: the same exploit, now failing. A new scan that finds nothing does not prove the old request is closed.
A regression test per object type
Keep one row per object type in your test suite.
| Test | Setup | Expected |
|---|---|---|
| Cross-tenant read | Tenant 2 account requests tenant 1's object | Denied, no body data |
| Same-tenant peer read | Account B requests A's private object | Denied unless sharing is on |
| Write and delete | Account B updates or deletes A's object | Denied; object unchanged |
| No session | Request with cookies cleared | 401 or 403 |
| List scoping | Account B lists the collection | Only B's records appear |
| Owner override on create | B posts with A's owner ID | Owner set from session |
Can scanners and code review find IDOR?
Scanners work well at known patterns and badly at ownership. A DAST tool with one login cannot tell that account B's invoice is exposed to account A, because it has no second account to compare. Why your scanner has one login explains this gap, and exploited, not suspected explains why proof matters.
Code review fills the gap. When you review a handler, ask: where does this ID come from, what query uses it, and what ties the result to the caller? A lookup that takes only the ID is the signal. The same pattern is easy to search for in code, and white-box testing uses it: read the code to find the lookups, then test the running app to confirm which ones really expose data. See white-box testing and the comparison of the three approaches.
What mistakes make IDOR tests miss bugs?
- Testing only GET. Writes and deletes are usually the bigger finding.
- Testing only the visible page. Exports, mobile and old API versions are separate routes.
- Using an admin as the second account. Admins are meant to see everything, so the test proves nothing.
- Stopping at the first fix. The same lookup pattern probably exists in other handlers.
- Testing once a year. New routes ship weekly; pair a yearly test with automated checks, the VAPT checklist and the web application checklist.
For a broader view of related flaws that surface beside IDOR, such as pricing and workflow abuse, see the business logic testing checklist. For proof from real findings, visit evidence, and for how a test runs end to end, how it works.
Questions people ask
Idor checklist: FAQs
What is an IDOR vulnerability?
An insecure direct object reference is a flaw where the app uses an ID sent by the client to fetch or change an object without checking that the caller may access it. Changing /orders/1042 to /orders/1043 and seeing someone else's order is the classic case.
How do you test for IDOR?
Use two accounts. Capture a request for an object account A owns, replace the ID with one owned by account B, keep A's session, and compare the response. Repeat for every verb, route and identifier location.
Do UUIDs prevent IDOR?
No. UUIDs make guessing impractical but do not check ownership; OWASP says access control checks stay essential. IDs leak through lists, links, logs and exports. Use UUIDs as an extra layer after the check.
Is IDOR the same as BOLA?
They describe the same weakness from different angles. IDOR is the older web term for a client-controlled reference. BOLA is the OWASP API Security Top 10 (2023) name, listed as API1. Both map to CWE-639.
Which CWE is IDOR?
CWE-639, Authorization Bypass Through User-Controlled Key, is the closest match. Related weaknesses are CWE-862 (Missing Authorization) and CWE-863 (Incorrect Authorization). Put the CWE and the OWASP category on the ticket.
Can automated scanners find IDOR?
Rarely on their own. Finding IDOR needs at least two accounts and knowledge of which object belongs to whom. A scanner with one login cannot tell that a valid response belongs to someone else, so manual or multi-account testing is needed.
How do I fix an IDOR?
Scope the database query to the caller or tenant, or run an explicit ownership check on every read, update and delete. Take identity from the session, deny by default, and add a regression test that fails if the check disappears.
How often should we test for IDOR?
Test whenever you add a route or change who owns data, and at least once a year with a full pentest. Automated two-account tests in CI catch new routes between pentests.
Sources
Where the facts come from
- OWASP: Web Security Testing Guide, Testing for Insecure Direct Object References
- OWASP: Insecure Direct Object Reference Prevention Cheat Sheet
- OWASP: Authorization Testing Automation Cheat Sheet
- OWASP: Top 10:2025, A01 Broken Access Control
- OWASP: API Security Top 10 2023, API1 Broken Object Level Authorization
- MITRE: CWE-639 Authorization Bypass Through User-Controlled Key
- MITRE: 2025 CWE Top 25 Most Dangerous Software Weaknesses
- HackerOne: The Rise of IDOR, 2021
- PortSwigger Web Security Academy: Insecure direct object references (IDOR)
- MDN: Insecure Direct Object Reference (IDOR)
Keep reading
Related articles
Access control testing checklist: sideways, upward and across tenants.
A step-by-step access control testing checklist for web apps and APIs: account planning, sideways, upward and cross-tenant checks, evidence and retests.
API security testing: a 2026 guide to the OWASP API Top 10
A step-by-step API security testing guide: the OWASP API Top 10 (2023) with test cases, BOLA and BFLA checks, JWT, OAuth, GraphQL and a CI/CD plan.
Black box vs white box penetration testing: which to choose
Black, grey and white box penetration testing compared: what each finds and misses, how effort differs, when to choose each and how to combine them.
Can your app pass the two-account test?
Cyberlop tests your web app and APIs in black-box, grey-box or white-box mode and checks cross-account access. It reports what it exploited, with the request, the response and, in white-box mode, the line of code. Ask us to run it on one of your applications.
Or start with a $199 pilot on one application: thirty days, success criteria agreed before day one, credited against the annual if you convert.