GUIDE / security
How to Test for Broken Access Control
Learn how to test for broken access control: IDOR, RBAC, vertical and horizontal privilege checks, API object access, and a practical BAC checklist.
Hidden buttons are not access control. How to test for broken access control focuses on identity, ownership, and verbs: can user A read user B's objects, can a standard role hit admin APIs, and does every valuable action enforce server-side rules for every actor.
This guide teaches a practical method to test for broken access control: horizontal and vertical checks, IDOR/BOLA probes, RBAC matrices, forced browsing, API-first testing, multi-tenant traps, common mistakes, and a release checklist. You will leave with test cases you can run in staging this week.
For related depth, read authentication and authorization testing, OWASP Top 10 for testers, the API security testing checklist, and security testing for QA.
Ethics and Safety
- Use authorized environments and test accounts you control.
- Prefer staging with synthetic personal data.
- Do not exfiltrate real customer content to prove impact.
- Stop at proof of access; do not bulk-download data.
- Coordinate if tests might trigger fraud or abuse systems.
What Broken Access Control Means
Access control answers:
- Who are you? (authentication)
- What are you allowed to do? (authorization)
Broken access control is primarily a failure of question two, often after question one succeeded. You have a valid login, but the server does not correctly limit your actions.
Common forms:
- Horizontal privilege escalation: peer user access.
- Vertical privilege escalation: lower role performs higher role actions.
- Context/tenant failures: correct role, wrong organization.
- Function-level failures: can invoke disallowed operations.
- Field-level failures: can modify protected attributes (role, balance).
- Unauthenticated access: protected resources available with no login.
Why UI Testing Is Not Enough
The UI may hide "Delete user" from non-admins. Attackers call:
DELETE /api/admin/users/55
with a normal user token. If the server trusts the client, the control is broken.
Rule: every authorization rule must be enforced server-side and tested without relying on the UI.
How to Test for Broken Access Control: Build the Authorization Model First
Before changing IDs at random, document intended rules.
Identify actors
Examples:
- Anonymous visitor
- Authenticated user
- Org member
- Org admin
- Support agent
- Superadmin
- Partner API client
Identify resources
Examples:
- Profile
- Order / invoice
- Document / file
- Project
- API key
- Webhook config
- Admin report
Identify actions
- List
- Read
- Create
- Update
- Delete
- Export
- Approve
- Share
- Transfer ownership
Build a matrix
| Action on Invoice | Anon | User (owner) | User (other) | Org admin | Superadmin |
|---|---|---|---|---|---|
| List | Deny | Own | Deny others | Org scope | All / policy |
| Read | Deny | Own | Deny | Org scope | Allow |
| Refund | Deny | Deny | Deny | Limited | Allow |
| Export PDF | Deny | Own | Deny | Org scope | Allow |
Each cell is a potential test case. This matrix is the heart of learning how to test for broken access control systematically.
Horizontal Testing: Peer Access and IDOR
Horizontal tests ask: can User B access User A's object?
Step-by-step IDOR method
- Log in as User A.
- Create or locate a resource owned by A. Note its ID.
- Log in as User B (same role, different owner).
- Attempt read, update, delete, share, and export on A's ID.
- Attempt list/search filters that might return A's object.
- Record status codes and whether data or side effects occurred.
Example targets:
GET /api/orders/1001
PUT /api/orders/1001
DELETE /api/orders/1001
GET /api/orders/1001/invoice.pdf
GET /api/orders?userId=A
GET /api/orders?search=A-PO-number
ID patterns to try
- Sequential integers: 1001, 1002, 1000
- UUIDs captured from other accounts (never assume UUID equals authorization)
- Emails or usernames in paths
- Nested IDs:
/projects/12/tasks/99 - Secondary keys: account numbers, invoice numbers, short codes
Expected secure outcomes
403 Forbiddenor404 Not Foundper product design- No sensitive body leakage on deny
- No state change on unauthorized write
- Consistent behavior across GET and mutation verbs
404 can be intentional to avoid existence leaks. 403 is also fine. 200 with someone else's data is a defect. 500 with stack traces is a separate quality and security issue.
Vertical Testing: Role Climbing
Vertical tests ask: can a lower privilege role perform higher privilege functions?
Method
- Capture admin-only requests while logged in as admin (or from API docs).
- Replay those requests with a standard user token.
- Replay with support role if that role is semi-privileged.
- Confirm denials for GET, POST, PUT, PATCH, DELETE, not only one verb.
Examples:
GET /api/admin/users
POST /api/admin/users/55/disable
PUT /api/admin/feature-flags
GET /internal/reports/revenue
Also test:
- Hidden front-end routes (
/admin,/settings/billing/advanced) - Feature-flagged panels that still call live APIs
- "Impersonate user" tools restricted to support
Function-Level Authorization
Sometimes a user can open an object but should not perform certain actions.
Examples:
- Can view order, cannot refund.
- Can comment, cannot delete thread.
- Can read employee directory, cannot export all emails.
- Can create tickets, cannot assign severity beyond a limit.
Test by mapping buttons to API calls, then calling disallowed functions directly.
Multi-Tenant and Organization Boundaries
SaaS products often fail here.
Fixtures:
- Org1 Admin, Org1 Member
- Org2 Admin, Org2 Member
Probes:
- Org2 member reads Org1 object by ID.
- Org2 admin lists with filter
orgId=Org1. - Invite flows that attach users to the wrong tenant.
- Switching tenant in UI while replaying old tokens.
- Shared resources and cross-tenant sharing rules.
Tenant ID in a JWT claim is not enough if handlers ignore it or trust client-supplied orgId in the body.
Field-Level and Mass Assignment Access Control
Broken access control includes writing fields you should not control.
Probe:
PATCH /api/users/me
{
"displayName": "B",
"role": "admin",
"accountBalance": 99999,
"emailVerified": true
}
Expected: protected fields ignored or rejected.
Also test create endpoints:
POST /api/projects
{
"name": "x",
"ownerId": "someone-else",
"plan": "enterprise"
}
Forced Browsing and Discovery
Find targets beyond the happy path:
- Sitemap, mobile app traffic, old admin bookmarks
- OpenAPI/Swagger docs in staging
- JavaScript bundles referencing admin routes
- robots.txt and public docs
- Parameter names in one endpoint reused in others
Then attempt access as lower privilege users.
API-First Testing Beats Click-Only Testing
Recommended workflow:
- Establish sessions/tokens for each actor.
- Build a collection of resource endpoints.
- Parameterize IDs and tokens.
- Run cross-actor request grids.
- Validate response bodies, not only status codes.
- Confirm side effects with a second read as the owner.
Browser testing still matters for:
- Client-side route guards that confuse teams (document that they are not security)
- Download flows and file viewers
- Multi-step UI workflows that produce complex payloads
But the authorization truth is on the server.
GraphQL and Batch Endpoint Notes
GraphQL can hide many operations behind one HTTP route.
Test:
- Unauthorized queries and mutations by name
- Object IDs in nested selections
- Batching that returns other users' nodes
- Introspection exposure policies
Example idea:
query {
invoice(id: "INV_OF_OTHER_USER") {
id
amount
customerEmail
}
}
If this returns data for an unauthorized user, you found BAC.
Files, Exports, and Share Links
Access control often breaks on secondary delivery paths:
- Direct file URLs
- Cloud storage signed URLs with long expiry and guessable paths
- CSV exports
- PDF generators
- Share links that never expire and are not revocable
- "Anyone with link" vs restricted visibility mismatches
Test:
- Owner creates file.
- Other user tries direct object URL.
- Expired share link denied.
- Revoked share link denied.
- Export includes only authorized rows.
Creating a Practical BAC Test Pack
Minimum actors
- UserA
- UserB
- Admin (or OrgAdmin)
Minimum resources
Pick your top three valuable objects (for example invoice, document, project).
Minimum actions per object
Read, update, delete, export/list.
Core grid
| # | Actor | Target | Action | Expected |
|---|---|---|---|---|
| 1 | UserB | UserA object | Read | Deny |
| 2 | UserB | UserA object | Update | Deny |
| 3 | UserB | UserA object | Delete | Deny |
| 4 | UserA | UserA object | Read | Allow |
| 5 | User | Admin function | Any | Deny |
| 6 | Anon | Protected object | Read | Deny |
| 7 | Org2 | Org1 object | Read | Deny |
| 8 | User | Protected field write | Patch role | Deny |
Expand from this skeleton until critical resources are covered.
Example Test Cases
| ID | Title | Preconditions | Steps | Expected | Priority |
|---|---|---|---|---|---|
| BAC-01 | Cross-user read denied | Orders for A and B | As B, GET A's order | Deny, no body leak | Critical |
| BAC-02 | Cross-user update denied | Same | As B, PUT A's order note | Deny, data unchanged | Critical |
| BAC-03 | Admin API denied to user | Capture admin list call | Replay as user | Deny | Critical |
| BAC-04 | Nested resource check | Project A with task 99 | As B, open task 99 URL | Deny | High |
| BAC-05 | Export scope | Many orgs | As org admin, export | Only own org rows | Critical |
| BAC-06 | Mass assignment role | User token | PATCH role=admin | Role unchanged | Critical |
| BAC-07 | Unauth access | None | GET protected without token | 401 | High |
| BAC-08 | Share link revoke | Shared doc | Revoke, open link as other | Deny | High |
| BAC-09 | Search leak | Distinct order numbers | As B, search A's order number | No A data | High |
| BAC-10 | Verb confusion | User can GET own | As B, POST action on A's id | Deny | High |
Status Code and Response Hygiene
While testing access control, also observe:
- Unauthorized responses that include stack traces
- Deny responses that still include object titles or emails
- Different timing that reveals object existence if that matters for your threat model
- Verbose "access denied for user X to invoice Y owned by Z" messages that leak ownership maps
Perfect existence hiding is not always required, but accidental data leakage in deny paths is still a defect.
How to Prove Impact Responsibly
Good proof:
- Show one unauthorized object read with two user IDs.
- Show one unauthorized state change and revert it.
- Redact personal fields in tickets if needed.
Bad proof:
- Dumping hundreds of records.
- Accessing real production customer data without authorization.
- Changing production admin passwords.
Filing a Strong Broken Access Control Report
Title: IDOR allows any authenticated user to read other users' invoices
Actors:
- userA@example.test owns invoice inv_1001
- userB@example.test is same role, different account
Request:
GET /api/v1/invoices/inv_1001
Authorization: Bearer <userB token>
Actual: 200 OK with line items, amount, billing email of userA
Expected: 404 or 403 with no invoice payload
Impact: Horizontal broken access control over billing data; privacy and trust risk; possible compliance impact
Scope notes: Also verified PUT is denied; issue appears read-path only
Remediation direction: enforce ownership/tenant checks in invoice read handler and shared policy layer; add regression tests for cross-user read
Evidence: sanitized screenshots and response JSON with PII masked
Common Mistakes When Testing Access Control
Mistake 1: Testing only with one user
Without UserA/UserB pairs, IDOR remains invisible.
Mistake 2: Trusting hidden buttons
UI is not a security boundary.
Mistake 3: Only testing GET
Writes and deletes are often more damaging.
Mistake 4: Only testing sequential IDs
UUID systems still need cross-user tests using captured IDs.
Mistake 5: Forgetting list and search
One search endpoint can leak many objects.
Mistake 6: Ignoring nested resources
Parent authorization does not always flow correctly to children.
Mistake 7: No multi-tenant fixtures
Many serious SaaS bugs are tenant confusions, not role confusions.
Mistake 8: Closing bugs without regression tests
BAC issues return when new handlers skip the policy layer.
Automation Strategy
Automate stable denials:
- Cross-user read on one representative resource type.
- User token against one admin endpoint.
- Unauthenticated access to one protected endpoint.
- Mass assignment attempt on profile.
Keep exploratory matrices for new modules. Pure automation without new-object coverage still drifts.
Broken Access Control Checklist
Actors and setup
[ ] At least two users same role
[ ] At least one higher privilege role
[ ] Second tenant/org if multi-tenant
[ ] Tokens/sessions documented
Horizontal
[ ] Cross-user read/update/delete tested on top resources
[ ] Nested IDs tested
[ ] List/search/export tested for leakage
[ ] Files and share links tested
Vertical
[ ] Admin/support endpoints replayed as lower roles
[ ] Forced browsing to privileged routes
[ ] Function-level actions denied correctly
Data shaping
[ ] Protected fields cannot be written
[ ] Client-supplied owner/org IDs cannot reassign access
Hygiene
[ ] Deny responses do not leak sensitive payloads
[ ] 401/403/404 behavior consistent with design
[ ] Issues filed with two-actor evidence
[ ] Regression cases added after fixes
How BAC Relates to Authentication Bugs
Sometimes a failure looks like access control but is authentication:
- Token not required at all
- Token from deleted user still works indefinitely
- Session fixation
Test both layers. Authn proves identity. Authz limits action. Guides like how to test JWT authentication and API authentication testing cover the identity side.
Team Workflow That Prevents BAC Recurrence
- Design: write access rules in the story.
- Implement: central policy helpers, not copy-paste checks.
- QA: matrix tests before release.
- Automate: critical denials in CI.
- Pentest/AppSec: periodic deep review.
- Incident learning: each escape becomes a regression fixture.
Broken access control is as much a process problem as a technical one.
Practice
Access control testing rewards discipline more than cleverness. Practice building actor matrices and two-user proofs. For structured QA skill practice, create an account at QABattle sign-up and train scenario discipline in battles.
How to Test for Broken Access Control on Real Sprints
Insert BAC work into ordinary delivery with minimal ceremony.
During story refinement
Ask:
- Who can do this action today?
- Who must never do this action?
- Is this object tenant-scoped, user-scoped, or global?
- Are there secondary delivery paths (export, email link, API, mobile)?
Write at least one denial acceptance criterion for sensitive stories.
During test design
Add matrix rows before scripted UI cases. Authorization denials are not optional extras.
During execution
Run cross-user API probes on the same day you run happy path UI tests. Do not defer all security to the end.
During regression
Keep a living pack of the top denied actions. Every fixed BAC bug adds a case.
Advanced Nested Authorization Examples
Example: project files
/projects/{projectId}/files/{fileId}
Tests:
- User in project can read file.
- User outside project cannot read file even with valid fileId.
- User removed from project loses file access on next request.
- Public share link rules override correctly and revocably.
- File IDs from project A do not work under project B paths if the server keys only on fileId.
Example: support tickets with internal notes
GET /tickets/88
GET /tickets/88/internal-notes
Customer user may read ticket but never internal notes. Support agent may read both. Test both endpoints, not only the parent.
Caching and CDN Access Control Pitfalls
Sometimes the app authorizes correctly on first request, then a cache serves content to the wrong user.
Checks:
- Authenticated responses are not stored in shared public caches.
Cache-ControlandVaryheaders make sense for sensitive GETs.- CDN configuration does not key only on URL without auth context.
- File download links are user-bound or short-lived signed URLs.
If User B receives User A content after a first authorized fetch by A, treat it as broken access control with infrastructure contributors.
Pagination and Aggregate Leakage
List endpoints can break access control without classic IDOR.
Examples:
- Global search returns other tenant hits.
totalCountreveals existence of hidden resources.- Sorting by private fields errors in ways that leak data.
- Analytics endpoints aggregate across tenants incorrectly.
Test with carefully seeded data so leakage is obvious.
Rate Limits Are Not Access Control
Throttling an admin endpoint does not mean a user is unauthorized. Confirm true authz denials, not only slow success.
Temporary Permissions and Time Boxes
Modern products grant temporary access:
- Support break-glass sessions
- Time-limited shares
- Trial admin privileges
- Scheduled access reviews
Test:
- Access works during the window.
- Access ends after expiry without waiting for UI refresh myths.
- Extensions are authorized.
- Audit logs capture temporary elevation if required.
Checklist Addition: Change Management Actions
High risk actions often missing from suites:
[ ] Transfer ownership
[ ] Move object between orgs
[ ] Bulk reassign
[ ] Merge accounts
[ ] Disable SSO for org
[ ] Rotate org-wide API keys
[ ] Change billing owner
These actions deserve explicit matrix rows because impact is huge.
Sample Automation Pseudocode
test cross_user_invoice_read_denied:
tokenA = login(userA)
tokenB = login(userB)
inv = create_invoice(tokenA)
res = get("/invoices/" + inv.id, tokenB)
assert res.status in {403, 404}
assert userA.email not in res.body
test user_cannot_list_admin_users:
token = login(standard_user)
res = get("/admin/users", token)
assert res.status in {401, 403}
Keep these tests boring and always on.
Coordinating with Bug Bounty and Pentests
When external researchers report BAC:
- Reproduce with your fixtures.
- Fix the path and search for sibling paths.
- Add regression.
- Ask: is this a missing framework control or one omitted handler?
- If systemic, schedule a horizontal review of all handlers for that resource type.
External reports are free curriculum for your internal matrix.
Metrics for BAC Health
Track:
- Number of production BAC incidents
- Number of staging BAC bugs found by QA pre-release
- Percent of new endpoints with explicit authz tests
- Time to fix critical IDOR
- Recurrence of same resource-type failures
If QA finds zero BAC issues all year on a complex multi-tenant app, your tests are probably too weak, not the product too perfect.
A Closing Drill You Can Run Tomorrow
Ninety-minute drill:
- Pick one resource type.
- Create objects as UserA.
- Attempt five unauthorized actions as UserB.
- Attempt two admin actions as UserB.
- Attempt one export.
- Document results in matrix form.
- File defects or residual risk notes.
Run this drill every time a new resource type ships. That habit is the operational answer to how to test for broken access control at team scale.
Conclusion
Learning how to test for broken access control means proving that every valuable action is limited by server-side rules for every actor. Build a permission matrix, pair users for horizontal tests, replay privileged functions for vertical tests, attack list/export/file paths, and never trust a hidden button as a control.
Broken access control is common because products change faster than permission checks. Make cross-user and cross-role denials part of ordinary regression, and you will catch the highest impact web security failures before attackers or customers do.
FAQ
Questions testers ask
How do you test for broken access control?
Create users with different roles and ownership, then attempt to access objects and functions outside each user's rights by changing IDs, forced browsing to hidden URLs, and calling APIs directly. Confirm the server denies unauthorized reads and writes, not only that the UI hides buttons.
What is the difference between horizontal and vertical privilege escalation?
Horizontal escalation is acting as a peer user, such as reading another customer's invoice. Vertical escalation is gaining higher privilege, such as a standard user calling an admin API. Both are broken access control and both need dedicated test cases.
What is IDOR?
Insecure Direct Object Reference is a common access control failure where an app uses a user-supplied object ID without verifying authorization. Changing /invoices/1001 to /invoices/1002 may expose another user's data if checks are missing.
Why is broken access control so common?
Modern apps have many roles, tenants, object relationships, and API endpoints. UI restrictions are easy; server-side checks on every path are harder. New features often add routes faster than authorization tests, so gaps ship unnoticed.
Can automated scanners find broken access control reliably?
Not reliably by themselves. Access control depends on business rules and multi-user context. Scanners help with some patterns, but human-designed role matrices and cross-user API tests catch most BOLA/IDOR issues.
What should a broken access control bug report include?
Include two actors, tokens or sessions used, exact requests, object ownership, expected denial, actual success or data returned, impact, and whether the issue is read, write, or function-level. Add sanitized evidence and a retest plan.
RELATED GUIDES
Continue the route
Authentication and Authorization Testing
Learn authentication and authorization testing: RBAC, IDOR, session management, vertical privilege escalation, and broken access control test cases.
OWASP Top 10 Explained for Testers
OWASP Top 10 for testers explained: what each risk means, how QA prioritizes checks, maps risks to cases, and builds a practical security regression suite.
API Security Testing Checklist
Use this API security testing checklist for auth, access control, injection, rate limits, headers, and transport checks QA can run before release.
Security Testing for QA Engineers: An Introduction
Learn security testing for QA engineers: what checks you can run, how it differs from functional testing, and a practical release checklist for web apps.