IDOR prevention checklist (authorization): what to review in 30 minutes
A practical, ops-style authorization checklist to prevent IDOR (broken object-level access control). Focus on where IDs enter, how reads/writes are scoped, consistent deny behavior, and one regression test that prevents reintroducing the bug.
Table of Contents
- Conclusion
- Explanation
- Practical Guide
- Step 0 (3 min): list the 3 objects that would be fatal to leak
- Step 1 (5 min): find every endpoint/action that accepts an ID
- Step 2 (8 min): verify read queries always enforce a boundary
- Step 3 (8 min): verify writes enforce the same boundary
- Step 4 (4 min): list/search endpoints are common leaks
- Step 5 (2 min): add one negative boundary test
- Pitfalls
- Checklist
- FAQ
- Q1. Is route-level auth middleware enough?
- Q2. Should I return 403 or 404 for unauthorized access?
- Q3. What’s the fastest way to prevent regressions?
- Internal links
- References
What should you review to prevent IDOR (broken authorization) in a web app?
Conclusion
In 30 minutes, you can eliminate most IDOR risk by verifying three things:
- Where IDs come from (URL, body, query) and assuming they can be tampered with
- Read boundaries include
owner_id/tenant_id(not onlyid = :id) - Write boundaries include the same boundary (UPDATE/DELETE scoped by owner/tenant)
Then you lock it with one regression test:
- “User B cannot read or mutate User A’s object”
If the checklist is clean, invest in RBAC/ABAC later.
Implementation examples may be available on DevSnips.
Explanation
Authentication answers “who are you?” Authorization answers “which object can you access?”
IDOR happens when a valid user can access another user’s resource by changing an ID. AI-assisted development increases the frequency because generated code often:
- protects routes with auth middleware
- but still queries
WHERE id = :id - without enforcing owner/tenant boundaries
UUIDs reduce guessing, not access control. Authorization must be enforced server-side.
Practical Guide
Step 0 (3 min): list the 3 objects that would be fatal to leak
Examples:
- billing: invoices, subscriptions
- business data: projects, documents, tickets
- admin: members, roles, invites
Decision rule:
- If leaking it causes a reportable incident, it’s in scope.
Step 1 (5 min): find every endpoint/action that accepts an ID
Look for:
GET /api/<object>/:idPATCH /api/<object>/:idDELETE /api/<object>/:idPOST /api/<object>/update(id in body)
Also search for IDs in payloads:
id,userId,orgId,tenantId,projectId
Decision rule:
- If the client can send it, it must be validated and bounded.
Step 2 (8 min): verify read queries always enforce a boundary
Your read query must include an owner/tenant scope.
- single-tenant:
WHERE owner_id = current_user_id AND id = :id - multi-tenant:
WHERE tenant_id = current_tenant_id AND id = :id
Anti-patterns:
findById(id)onlyWHERE id = :idonly- boundary derived from user input (
tenant_id = body.tenantId)
Step 3 (8 min): verify writes enforce the same boundary
Reads are not enough. Writes must be scoped too.
- UPDATE/DELETE
WHEREincludesowner_id/tenant_id - treat “0 rows updated” as deny (403/404) per policy
Anti-patterns:
- read is checked, write is
WHERE id = :id - two-step checks outside a transaction (TOCTOU)
Step 4 (4 min): list/search endpoints are common leaks
Examples:
GET /api/<object>?q=...GET /api/<object>/recent
Checklist:
- does every query include tenant/owner scoping?
- do you have a shared query helper or default scope?
Step 5 (2 min): add one negative boundary test
Minimum:
- User B cannot read User A’s object
- User B cannot mutate User A’s object
Decision rule:
- If you can’t write this test, your boundary is not centralized enough.
Pitfalls
- “We hide it in the UI” (not a control)
- trusting client-provided
tenantId/orgId - inconsistent deny behavior (some routes 403, others 200)
- background jobs/webhooks mutate by ID without the same boundary
- logging raw payloads and leaking PII/secrets
Checklist
- [ ] Top 3 high-impact object types are listed
- [ ] All ID entry points are inventoried (URL/body/query)
- [ ] Read queries include owner/tenant boundaries
- [ ] Write queries include the same boundaries
- [ ] Client-provided boundary fields are never trusted (
tenantId,orgId) - [ ] Deny semantics are standardized (403 vs 404)
- [ ] List/search endpoints are scoped by owner/tenant
- [ ] A shared helper exists for boundary-scoped queries
- [ ] Negative boundary tests exist (B cannot access A)
- [ ] Denial logging exists without leaking PII/secrets
- [ ] AI-generated code touching authz requires explicit review gates
FAQ
Q1. Is route-level auth middleware enough?
No. Middleware answers “is the user logged in?”. IDOR is about “which object can they access?”, which must be enforced in the handler/data layer.
Q2. Should I return 403 or 404 for unauthorized access?
Either can be correct. Pick one based on your threat model and auditing needs, then apply it consistently across the app.
Q3. What’s the fastest way to prevent regressions?
One regression test per object type: “User B cannot access User A’s object”. This prevents reintroducing IDOR during refactors or AI-assisted changes.
Internal links
- Parent hub: AI development: start here
- Related:
References
- OWASP Top 10 (A01: Broken Access Control): https://owasp.org/www-project-top-ten/
Popular
- 1Permit2 explained (Web3): why approvals changed and how to use it safely (checklist)
- 2Read wallet signing screens (Web3): a 30-second checklist to avoid permission traps
- 3Spec-to-implementation prompt template (AI development): how to stop the model from guessing
- 4Revoke token approvals on EVM: how to audit allowances safely (checklist)
- 5Clarifying questions checklist (AI development): what to ask before you let an LLM build