Last reviewed: 2026-10-16
Direct answer
Prevent insecure direct object references (IDOR) in coding-agent API patches by checking whether the authenticated caller may perform the requested action on the specific object before returning data or causing an effect. Derive identity from trusted authentication, constrain object lookup to the permitted scope, and enforce the application’s object policy. Authentication alone, a valid UUID, or access to the endpoint does not establish that permission.
The OWASP IDOR prevention guide recommends looking up records through datasets the user can access and verifying permission on every access attempt. OWASP API1:2023 requires checks for the requested action on the requested object wherever client input selects a record. MITRE CWE-639 describes the underlying failure: changing a user-controlled key exposes another user’s record because authorization does not enforce the caller’s entitlements.
Review the generated diff against a written access policy, then prove it with multiple authenticated test users. A passing owner-only test leaves the central boundary untested.
Who this is for
This guide is for maintainers reviewing agent-written REST handlers, GraphQL resolvers, document downloads, export jobs, and repository methods that accept object identifiers. Start when a patch adds a lookup, changes its scope, moves a permission check, or reuses an existing service from a new entry point.
For example, an agent adds GET /workspaces/{workspaceId}/documents/{documentId}. The handler requires login and calls findById(documentId). The owner’s request works, but another logged-in user can supply the same document ID. The useful review question is which server-side rule connects this caller, workspace, document, and read action.
Key takeaways
- Treat every caller-supplied object reference as a selector requiring authorization, including references in bodies, headers, filenames, and nested routes.
- Obtain the principal from validated authentication. A submitted
userId,ownerId, or tenant header cannot grant its own authority. - Scope the lookup and check the action. Membership in a tenant does not automatically grant access to every private record within it.
- Check every referenced object in a bulk operation and verify parent-child relationships in nested routes.
- Use a known valid foreign identifier in negative tests. Random nonexistent IDs prove only missing-record behavior.
- Assert that denied requests disclose no protected data and produce no unauthorized mutation, export, or queued work.
Sources checked
- OWASP: Insecure Direct Object Reference Prevention Cheat Sheet
, fetched successfully on 2026-10-16. It supports scoped lookups, permission checks on every access, multi-account verification across operations, and the limits of complex identifiers. Its HTTP
Last-Modifiedvalue was 2026-10-15; that delivery timestamp does not establish a new security announcement. - OWASP API Security: API1:2023 Broken Object Level Authorization , fetched successfully on 2026-10-16. It supports action-and-object authorization, references beyond URL parameters, a multi-document GraphQL example, and regression tests. This remains the project’s published 2023 edition; its HTTP representation reported a September 2026 modification timestamp.
- MITRE: CWE-639, Authorization Bypass Through User-Controlled Key , fetched successfully on 2026-10-16 as CWE 4.20. It supports the weakness definition and per-access entitlement checks. The page states that it was last updated on 2026-04-30 and notes that the relationship between IDOR and broken object-level authorization needs further investigation.
These are stable primary technical references for an evergreen review task. The example policy, response convention, workflow, and acceptance matrix below are engineering recommendations derived from them, rather than a report of a newly disclosed vulnerability.
Contract details to verify
Define permission before inspecting the helper name
Write a small policy table for the changed operation. Identify the principal, trusted tenant context, object relationship, action, allowed result, and denial behavior. A helper named authorizedFind is useful only when its implementation enforces those exact rules.
Use a synthetic document service as the running example. Alice and Bob belong to workspace Red and each own a private document. Eve belongs to workspace Blue. Carol also belongs to Red and has an explicit read-only grant to Alice’s document. The policy permits owners to read and update their documents, permits Carol to read the shared document, and denies other access. Workspace membership alone grants no access to private documents.
| Contract element | Required decision |
|---|---|
| Principal | Which trusted authentication component supplies the caller’s identity? |
| Workspace | How does the server establish the caller’s current membership or permitted scope? |
| Object | Must the selected document belong to the workspace named in the route? |
| Action | Which ownership or explicit grant permits read, update, delete, or export? |
| Denial | What public response and absence of side effects must tests observe? |
| Policy failure | Does an unavailable or invalid decision stop access? |
For this example, choose 401 for unauthenticated requests and the same 404 response for authenticated missing or denied objects. That convention reduces existence disclosure through response content; it does not by itself guarantee identical timing or remove every side channel. Applications that intentionally disclose existence may choose a documented 403 policy instead.
Put the check before disclosure and effects
This framework-neutral pseudocode expresses the read-path obligations:
principal = authentication.require(request)
workspace = access.requireWorkspace(principal, request.workspaceId)
document = documents.findWithinWorkspace(workspace.id, request.documentId)
if document is missing:
return notFound()
if not policy.allows(principal, "document:read", document):
return notFound()
return serializeApprovedDocumentFields(document)
requireWorkspace must establish server-side access to that workspace. findWithinWorkspace must enforce the document’s relationship to it. policy.allows must evaluate the actual caller, action, and document using trusted ownership or grant data. Implement these obligations through the application’s framework and data layer; the names alone enforce nothing.
For updates, authorize the update action before changing the object. Keep the authorization decision valid through the mutation using the application’s transaction, locking, or conditional-write design. Review ownership changes and concurrent grant revocation against that design. A separate check followed by an unrelated write needs an explicit consistency argument.
Request fields remain untrusted after authentication. Do not let an update body change ownerId, workspace membership, or a permission field unless a separately authorized operation permits that change. Returning an object through a download or serializer is also an access decision, even when no database write occurs.
Trace alternate entry points
Follow changed repository calls into resolvers, background workers, export builders, and download handlers. A correctly protected HTTP route cannot establish authorization for a second caller of the same unscoped service.
For queued work, define whether authorization must hold at submission, execution, or both, and how revocation affects pending work. For caches, verify that a response cached for an owner cannot be returned to another caller without the required access decision. These are review extensions of the per-access rule; their implementation depends on the application’s access and revocation contract.
Keep query safety alongside this review. The site’s SQL injection guide explains parameter binding. A perfectly parameterized lookup can still return an unauthorized document when its authorization predicate is missing.
Happy-path operator workflow
- Freeze the proposed revision and inventory changed object selectors, including IDs hidden inside bulk bodies and nested input objects.
- Write the permission table with the API owner. Include legitimate sharing or delegation so the repair preserves intended access.
- Trace each selector through lookup, policy evaluation, serialization, mutation, and job submission. Mark the first disclosure or effect.
- Implement scoped lookup and action-specific authorization before that boundary. Make absent context, denial, and policy errors terminate safely.
- Create synthetic users and records with different ownership and tenant scopes. Retain exact known identifiers for negative tests.
- Exercise the real request path for allowed and denied cases. Inspect both the response and resulting database, queue, and export state.
- Attach the policy version, fixture labels, test results, and revision to the pull request. Recheck the evidence after agent follow-up changes.
Use the site’s security scan gate alongside these tests. Business-specific ownership and sharing rules still need an explicit oracle, even when scanning reports no findings.
Error-path operator workflow
If Alice can read Bob’s private document, block the patch and preserve a minimal synthetic reproduction tied to the revision. Trace sibling operations using the same lookup; a read-path repair does not clear delete or export automatically.
Repair the access boundary, reset the disposable fixture state, and rerun allowed and denied cases. Do not replace sequential IDs with UUIDs and treat the finding as closed. OWASP explicitly says complex identifiers still require access checks when an unauthorized caller obtains a valid reference.
If the policy engine fails, return the documented controlled failure without returning the object or continuing the mutation. Do not retry through an unscoped repository helper. If a request may already have queued work or changed state, reconcile that state before retrying.
Keep the review record small: revision, route identifier, action, policy version, synthetic principal and object labels, expected decision, actual response class, and side-effect counts. Exclude credentials and real object contents. A suspected production exposure belongs in the service’s incident process; reproduce the patch defect with authorized synthetic fixtures.
Prove the policy with a test matrix
Run these as integration tests through the application’s actual authentication and authorization wiring. Direct unit tests of a policy helper are useful, but they cannot prove the handler invokes it before returning data.
| Request under the example policy | Expected evidence |
|---|---|
| Alice reads and updates her Red document | Intended response and exact authorized change; normal access remains usable. |
| Alice supplies Bob’s known Red document ID | Uniform denied response; no private fields, mutation, or queued work. |
| Alice supplies Eve’s known Blue document ID | Denied, including attempts to change the workspace route or submitted tenant field. |
| Carol reads Alice’s shared document, then attempts an update | Read succeeds; update is denied and leaves the document unchanged. |
| A permitted workspace path contains a document from another workspace | Relationship mismatch is denied before serialization or effects. |
| An authenticated request selects a missing document | Matches the chosen public missing-or-denied convention. |
| A request lacks authentication | Authentication failure; no protected response or effect. |
| A bulk request mixes permitted and forbidden IDs | Every object is checked; behavior matches the declared atomic or partial-result contract. |
| Access is revoked between two requests | The next request follows the documented revocation contract, including cache behavior. |
| The policy service errors or required context is absent | Controlled failure; no permissive fallback and no protected side effect. |
For bulk writes, specify atomic rejection or explicitly authorized partial processing before implementation. With atomic rejection, assert zero mutations and zero submitted jobs if any item is forbidden. With partial processing, assert that only authorized items change and that per-item responses do not expose protected foreign data. Do not let iteration order decide the contract.
For reads, check protected field values, response bodies, download bytes, and relevant metadata. For writes, assert exact before-and-after state and queue counts. An error status after a successful mutation is still a failed authorization boundary. These are proposed acceptance tests; no execution against a reader’s application is claimed.
Failure modes
Login middleware is treated as object authorization. It establishes who called. Trace the separate decision that permits this action on this record.
Tenant scope replaces the complete policy. Alice and Bob share Red, but private records remain private. Check ownership or grants within the tenant boundary.
A parent route is authorized while the child ID is loaded globally. Bind the child’s actual relationship to the authorized parent and apply its own object policy.
Read permission is reused for every action. Carol’s grant permits a read, not an update. Name the action in the policy call and test the distinction.
A bulk request checks only its first ID. Mixed fixtures must prove that each selected object receives the intended decision before effects.
A denied handler has already serialized, cached, or queued the object. Move the decision ahead of that boundary and assert absence of those effects.
Tests use only an administrator or nonexistent foreign IDs. Use ordinary users, known existing records, and an explicit expected policy for each case.
FAQ
Are IDOR and broken object-level authorization identical?
They overlap in common API reviews, but avoid treating them as exact synonyms. OWASP API1 describes object-level authorization broadly. MITRE CWE-639 specifically describes bypass through a user-controlled key and records that the relationship between the terms needs further investigation. Classify the observed data flow precisely.
Does comparing the session user ID with the requested ID solve this?
Only for a narrow operation whose referenced object is that user record and whose policy actually requires that comparison. A document ID identifies a document; projects, delegated access, and shared resources require their own relationships and rules. OWASP API1 explicitly warns that the simple comparison covers only a subset of cases.
Must authorization happen inside the database query?
OWASP recommends scoped lookups and also illustrates an explicit post-fetch check. Either design must prevent disclosure and effects until access is established. Verify action-specific policy, alternate callers, error handling, and consistency through mutations rather than accepting one method name as sufficient.
Are UUIDs still useful?
Unpredictable identifiers can reduce guessing as defense in depth. They do not authorize a caller who learns a valid identifier. Keep a known foreign UUID in the regression suite so the test exercises permission rather than guessability.
Reader next step
Choose one object-selecting operation in the next coding-agent patch. Write its caller-object-action policy and create two ordinary users with private records. Test a known foreign identifier through the real request path, then add the nested-route, sharing, and bulk cases the feature supports. Approve the revision only when legitimate access works and every denied case leaves protected data and effects outside the caller’s reach.