Two users open the same application. Each can sign in, each has a profile, and each can see a document list. Everything looks normal until one user changes a document identifier and receives a file belonging to the other.
This is the kind of mistake a two-account lab makes visible. Broken access control happens when an application permits an action outside the requesting user's permissions. Insecure direct object reference, usually shortened to IDOR, is one way that failure appears: a client supplies a resource identifier and the server returns or changes the resource without enforcing the required access rule.
Broken Access Control remains A01 in the OWASP Top 10:2025. The ranking gives useful context, but the practical question is local: can this account perform this action on this resource?
Separate signing in from permission to act
Authentication establishes who is making a request. Authorization decides what that identity can do. A valid session answers the first question; the server still needs to answer the second for the requested object and operation.
Suppose a practice application stores private reports. Alice owns report 410 and Bob owns report 411. Both accounts are ordinary users in separate teams. The policy says owners can read their own reports, and a team administrator can manage reports within their team. Being signed in should not let Alice read Bob's report.
A hidden button cannot enforce this rule. The browser is a place to present the interface, and its requests remain under the user's control. The decision belongs in trusted server code, with the account identity derived from the verified session.
Set up two accounts and known resources
Create Alice and Bob in separate browser profiles. Give each account a private report with a distinctive title. Record which account owns which report, and confirm that each owner can open their own report. This establishes a baseline before you test a boundary.
Keep the browser profiles separate throughout the exercise. Switching accounts in one tab can leave you looking at a cached screen or a session you did not intend to use. In your notebook, label each observation with the active identity and the resource owner.
The OWASP WSTG's IDOR testing chapter describes testing object references using known resources and multiple users. That approach helps distinguish an access-control failure from a nonexistent resource.
Write the expected permission matrix first
Before changing requests, state the policy as a table. This prevents a common mistake: calling access “unauthorized” without first establishing whether the product intentionally shares the resource.
| Requester | Resource | Action | Expected result |
|---|---|---|---|
| Alice | Alice's report 410 | Read | Allowed; returns Alice's report. |
| Alice | Bob's report 411 | Read | Denied; no private report content. |
| Bob | Alice's report 410 | Read | Denied; no private report content. |
| Signed out | Alice's report 410 | Read | No private report content. |
| Alice | Bob's report 411 | Edit | Denied; Bob's report remains unchanged. |
| Team A admin | Team B's report | Manage | Denied across the team boundary. |
This example distinguishes ownership, action, role, and team. A real application's policy may also depend on membership, publication state, a paid entitlement, or an explicit sharing grant. Record those conditions before testing them.
Run one controlled read test
While signed in as Alice, inspect the request that opens her report:
GET /api/reports/410
Session: Alice's verified session
Expected: Alice's own private report
In the assigned lab, make the same request for report 411 while retaining Alice's session. Change the resource reference only. Do not change the session and resource at the same time, because the result would no longer isolate the ownership boundary.
GET /api/reports/411
Session: Alice's verified session
Resource owner: Bob
Expected: refusal with no private content
Inspect the response body as well as the status. A 200 response containing an empty application shell is different from a 200 response containing Bob's private report. A 403 response that includes the report in an error object still leaks data. The useful evidence is the protected content or action that crossed the boundary.
Repeat the test in the opposite direction using Bob's profile and Alice's report. Then check the signed-out case. Record each result separately; a passing check in one direction does not establish that all paths enforce the policy.
Rule out the common false positives
If the application returns unexpected data, confirm the resource's owner and sharing state. A public report, a shared team document, or an administrator account may legitimately have wider access. These details belong in your test setup.
Check whether the browser displayed an earlier response from its cache. Compare the actual network response with the visible page. Also verify the active identity through the application's normal account view or a documented session endpoint.
A refusal for an invented ID provides little evidence about ownership checks. That resource may simply not exist. Using two known resources gives you a meaningful comparison.
Finally, treat read and write access as separate permissions. A successful read test does not tell you whether a report can be renamed, exported, or deleted. In a disposable lab, test a reversible edit and check the stored state afterward. A “success” message alone does not prove that a change occurred.
Fix the decision where the resource is loaded
A narrow private-report rule can combine resource lookup and ownership. The following is pseudocode for the invented lab, rather than a complete production implementation:
actor = requireVerifiedSession(request)
report = database.findReport({
id: request.params.reportId,
ownerId: actor.userId,
teamId: actor.teamId
})
if report is absent:
return notFoundWithoutPrivateContent()
return safeReportProjection(report)
Notice where the owner and team values come from: the verified server context. Accepting ownerId from the browser would let the request choose its own permission evidence. If the application supports sharing or administrative access, the lookup must reflect that actual policy rather than assuming ownership is the only valid relationship.
The OWASP IDOR Prevention Cheat Sheet explains that complex identifiers can reduce guessability but do not replace permission checks. Changing 411 to a UUID leaves the same flaw if a user who obtains that UUID can still read the report.
The Authorization Cheat Sheet recommends denying by default and validating permissions consistently. Apply the rule to every path that reaches the resource, including exports and downloads, rather than only the screen where the document appears.
Turn the matrix into regression checks
After the fix, run the original allowed case and the denied cross-account case. Both matter. Blocking every request prevents a leak by breaking the feature; the goal is the intended permission rule.
Use the table as a regression checklist. Assert that the legitimate owner receives the expected report. Assert that the other account receives no private fields. For a denied edit, reload the resource under its owner and verify that the persisted title is unchanged.
Give team and role boundaries their own fixtures. Use known objects in separate teams, and label the intended grants. The OWASP Authorization Testing Automation Cheat Sheet provides further guidance on representing and checking authorization rules. Your lab matrix is a small starting point for that work.
Write a finding someone else can reproduce
A useful report tells a developer which decision failed. Include the intended policy, requester, resource owner, endpoint, sanitized request, observed private content or state change, and a proposed retest. Remove credentials and keep the example data synthetic.
Finding: Alice can read Bob's private report
Expected policy: each ordinary user reads only their own report
Setup: Alice owns 410; Bob owns 411; neither report is shared
Action: Alice requests GET /api/reports/411
Observed: response includes Bob's unique report title
Impact demonstrated: one known private report crossed ownership
Fix to verify: server-enforced resource permission on all read paths
Retest: owner allowed; other user denied without private content
Keep the impact statement tied to what you demonstrated. Reading one report does not establish that every account or every endpoint is affected. If more testing is needed, say which part of the policy remains untested.
If this is your first exercise, start with our beginner cybersecurity lab plan. For a related training topic, explore the web security learning path overview and look for an available exercise with a clearly stated authorization objective.
Questions about IDOR testing
Is every changed ID an IDOR vulnerability?
No. The ID must lead to access or an action that violates the intended permission policy. A nonexistent object, deliberately public resource, or valid sharing grant can explain the result without an IDOR flaw.
Does hiding an admin page fix broken access control?
The server must enforce the permission when the action is requested. Removing a navigation link changes discoverability, but a user can still send a request to a known endpoint.
Should denied requests return 403 or 404?
The choice depends on the application's response policy, including whether it conceals resource existence. For this exercise, check that the response contains no private data and that the denied action did not change stored state. A status code alone is insufficient evidence.
Sources and further reading
Primary references checked on 9 October 2026. Worked scenarios and practice plans are illustrative teaching examples; they are not reports of incidents or tests on production systems.
- OWASP Top 10:2025 A01: Broken Access Control
- OWASP WSTG 4.2: Testing for Insecure Direct Object References
- OWASP Authorization Cheat Sheet
- OWASP Insecure Direct Object Reference Prevention Cheat Sheet
- OWASP Authorization Testing Automation Cheat Sheet
Found an error? Send a correction to VulnOS with the section and a supporting reference.

