Broken Access Control is the most prevalent application vulnerability in existence. OWASP found it in 94% of applications tested for the 2025 list. It has held the #1 position since 2021. Yet it remains one of the hardest to automatically detect at scale — because the definition of "correct" access requires understanding business logic that no generic tool possesses.
This post covers what the vulnerability class actually includes, how to detect it programmatically, and how to remediate it systematically.
What Broken Access Control Actually Covers
The category is broader than most developers realize. It includes:
Insecure Direct Object Reference (IDOR) The most common variant. Accessing resources by changing a predictable identifier:
GET /api/invoices/1047 → returns YOUR invoice
GET /api/invoices/1046 → returns ANOTHER USER's invoice (IDOR)
Forced Browsing Navigating to URLs that should require elevated privilege but don't enforce it server-side:
GET /admin/users → should require admin role
GET /reports/all → should require manager role
Method-Based Access Control Bypass An endpoint may block POST but not PUT, or block DELETE but not PATCH:
DELETE /api/users/42 → 403 Forbidden
PUT /api/users/42 → 200 OK (same effect, no check)
Privilege Escalation A standard user accessing or modifying resources that belong to admin or other roles:
PATCH /api/users/me {"role": "admin"} → should fail, sometimes doesn't
JWT Claim Manipulation Modifying JWT payload to escalate privileges when signature validation is absent or weak:
{ "userId": 42, "role": "user" } → { "userId": 42, "role": "admin" }
CORS Misconfiguration An overly permissive CORS policy allows a malicious origin to make credentialed requests:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true ← critical misconfiguration
Why Automated Detection Is Partial
The reason Broken Access Control remains prevalent despite extensive tooling: authorization rules are business logic, not syntax. A scanner can inject ' and detect SQL injection because the injection signal (database error) is universal. It cannot know that /api/invoices/1046 belongs to a different user unless it understands user identity and resource ownership.
What automation CAN detect:
| Test | Automated? |
|---|---|
| IDOR via ID enumeration (sequential IDs) | Yes |
| IDOR via UUID prediction (UUIDs are hard to predict) | No |
| Forced browsing (known path list) | Partially |
| Method bypass (test all HTTP methods on each endpoint) | Yes |
| JWT algorithm confusion (alg:none attack) | Yes |
| CORS misconfiguration detection | Yes |
| Horizontal privilege escalation (User A accessing User B's data) | Partial (requires two accounts) |
| Vertical privilege escalation (user accessing admin functions) | Partial |
The partial cases require two authenticated sessions — a standard user and an elevated user — so the scanner can test whether the standard user can access the elevated user's resources. PentestCheck supports multi-session authentication configuration for exactly this purpose.
Testing Strategy: The Two-Account Method
The most effective automated approach for IDOR:
- Configure scan with two accounts:
user_a(standard) anduser_b(standard) - Crawl the application as
user_a, collecting all resource URLs (documents, invoices, orders, profiles) - Replay each resource request as
user_b - Flag any 200-response to a resource that belongs to
user_a's session
This catches horizontal IDOR reliably. For vertical escalation:
- Configure with
user_standardanduser_admin - Crawl as
user_admin, collecting admin-only URLs - Replay admin-only URLs as
user_standard - Flag any 200-response that should require admin
Common Patterns and Root Causes
Pattern 1: Object-Level Authorization Missing at Service Layer
The controller checks authentication (is the user logged in?) but not authorization (does this user own this object?):
# Vulnerable
@app.route('/api/invoices/<int:invoice_id>')
@login_required # checks authentication only
def get_invoice(invoice_id):
return Invoice.get(invoice_id) # no owner check
# Secure
@app.route('/api/invoices/<int:invoice_id>')
@login_required
def get_invoice(invoice_id):
invoice = Invoice.get(invoice_id)
if invoice.owner_id != current_user.id:
abort(403)
return invoice
Pattern 2: Frontend-Only Access Control
The UI hides admin navigation from non-admin users. The API endpoint itself is unprotected because "users can't see the link."
Security assumption: if users can't navigate to it, they can't call it. Reality: attackers don't use your UI.
Pattern 3: Sequential Identifiers
UUIDs (random, 128-bit) are not practically enumerable. Auto-increment integers are. If your API uses integer IDs for resources, IDOR is trivial to test:
/api/documents/1000
/api/documents/1001
/api/documents/1002
The fix is using non-sequential identifiers (UUIDs or opaque tokens), but the real fix is always server-side authorization. Obscuring IDs is defense-in-depth, not a security control.
Remediation Framework
Short-term (critical, same sprint):
- Add server-side ownership verification to every object-fetching endpoint
- Block all unintended HTTP methods via allowlist (accept GET/POST only unless PUT/PATCH/DELETE is explicitly intended)
- Audit JWT validation — ensure signature verification is enforced,
alg:noneis rejected
Medium-term (within 30 days):
- Implement attribute-based access control (ABAC) or policy-based authorization at the service layer
- Migrate sequential IDs to UUIDs for external-facing resources
- Audit CORS configuration — never combine
Allow-Origin: *withAllow-Credentials: true
Long-term (next quarter):
- Integrate the two-account IDOR test into your CI/CD pipeline
- Implement centralized authorization middleware so ownership checks happen at one layer, not duplicated in every controller
- Add authorization decision logging (who accessed what, when) for incident response capability
Measuring Progress
Track remediation using:
- Count of open IDOR findings by resource type
- Time-to-remediate for A01 findings vs. total security findings
- Monthly two-account scan results trend
A mature access control program shows IDOR findings discovered in CI/CD (before production) rather than in production scans. That shift means your authorization model is being tested before it's exploitable.
PentestCheck tests for IDOR using multi-account session replay across your full application scope. Broken Access Control findings include the exact request that demonstrates exploitability.