Broken Access Control Testing: A Practical Pentesting Guide

Broken Access Control Testing: A Practical Pentesting Guide

If I had to pick one vulnerability category that shows up in nearly every single web application assessment I run, it’s broken access control. It consistently tops the OWASP Top 10, and unlike some vulnerability classes that require complex payloads, most access control bugs are found simply by asking “what happens if I change this ID, this role, or this request method?” In this guide, I’ll walk through a practical methodology for testing broken access control — the theory behind it, how I test for it systematically, and what actually fixes it.

What Broken Access Control Is and Why It Matters

Broken access control occurs when an application fails to properly enforce restrictions on what authenticated (or unauthenticated) users are allowed to do or see. It’s a broad category that covers several related failure modes:

  • Insecure Direct Object References (IDOR) — accessing another user’s data by manipulating an identifier.
  • Vertical privilege escalation — a low-privilege user gaining access to admin-only functionality.
  • Horizontal privilege escalation — a user accessing another user’s data at the same privilege level.
  • Missing function-level access control — an endpoint exists and works, but was never actually protected by a role check.
  • CORS misconfiguration — overly permissive cross-origin policies exposing authenticated data to untrusted origins.

This matters because access control failures directly translate to data exposure or unauthorized actions — and they’re often trivial to exploit once found, requiring no special tools beyond a browser or Burp Suite’s Repeater. A single unprotected admin endpoint can be far more damaging in practice than a technically “harder” vulnerability like a complex injection chain.

Lab Setup for Legal Practice

I test access control techniques exclusively against applications I control or am explicitly authorized to test:

  1. OWASP Juice Shop and DVWA, both of which include IDOR and privilege escalation challenges.
  2. PortSwigger’s Web Security Academy, which has an extensive, well-structured set of access control labs.
  3. A custom test app with multiple user roles (admin, standard user, guest) that I build myself when I want to practice against realistic multi-tenant scenarios.
  4. Burp Suite, with the Autorize and Auth Analyzer extensions, which automate a huge chunk of repetitive access control testing.
docker run --rm -p 3000:3000 bkimminich/juice-shop

What this does: launches Juice Shop locally, giving you a legal target with several deliberately broken access control challenges to practice against.

Methodology: Step-by-Step Access Control Testing

Step 1: Map Roles and Privilege Levels

Before testing anything, I identify every distinct privilege level the application supports — anonymous, standard user, premium user, moderator, admin — and create a test account for each one where possible. Understanding the intended access model is essential before you can identify where it’s broken.

Step 2: Test for IDOR (Horizontal Privilege Escalation)

I log in as a standard user, perform an action that touches an object with an identifier (viewing an order, downloading an invoice, editing a profile), and capture that request in Burp.

GE T /api/orders/1042 HTTP/1.1
Authorization: Bearer <user_A_token>

I then modify the identifier while keeping User A’s session token:

GE T /api/orders/1043 HTTP/1.1
Authorization: Bearer <user_A_token>

Purpose: if this returns User B’s order data despite being authenticated as User A, it confirms an IDOR vulnerability — the server is trusting the object ID in the request rather than verifying that the authenticated user actually owns that object.

For less predictable identifiers (UUIDs instead of sequential integers), I check whether IDs are exposed elsewhere in the application — API responses, URLs, or even client-side JavaScript — that would let an attacker harvest valid IDs to target.

Step 3: Test for Vertical Privilege Escalation

I log in as a standard user and attempt to directly access endpoints that should be admin-only, based on naming conventions or JavaScript source review:

GE T /admin/dashboard HTTP/1.1
Authorization: Bearer <standard_user_token>

Purpose: this checks whether server-side authorization is actually enforced on admin routes, or whether the application relies solely on hiding the admin link in the UI — a common and serious mistake known as “security through obscurity” on the frontend.

I also test admin-only API actions directly, not just page routes:

POST /api/users/1042/role HTT P/1.1
Authorization: Bearer <standard_user_token>
Content-Type: application/json

{"role": "admin"}

Purpose: this tests whether a role-modification endpoint validates that the requester has admin privileges, or only checks that the requester is authenticated at all — a distinction that’s frequently missed.

Step 4: Test for Missing Function-Level Access Control

Many applications protect the UI (hiding admin buttons from non-admins) without protecting the underlying API. I use Burp’s site map, built from crawling the app as different roles, to compare what endpoints exist versus what’s actually reachable by each role.

The Autorize extension automates this beautifully: you configure it with a low-privilege session token, then browse the app as an admin — Autorize automatically replays every request using the low-privilege token and flags any that still succeed.

Step 5: Test HTTP Method and Parameter Tampering

Sometimes access control is enforced on one HTTP method but not another for the same endpoint:

DELETE /api/users/1042 HT TP/1.1
Authorization: Bearer <standard_user_token>

Purpose: an endpoint might correctly block a standard user from geting another user’s admin panel, but forget to apply the same check to a DELETE or PUT request against the same resource path.

I also test parameter pollution and hidden parameters, such as adding an is_admin or role field to a registration or profile-update request that wasn’t part of the documented form:

POST /api/register HTT P/1.1
Content-Type: application/json

{"username":"testuser","password":"Pass123!","role":"admin"}

Purpose: this tests for mass assignment vulnerabilities, where the backend blindly binds all submitted fields to the user object without an explicit allowlist of permitted fields.

Step 6: Test CORS Configuration

I check the application’s CORS headers, particularly on endpoints that return sensitive data:

curl -H "Origin: https://evil-lab.local" -I https://lab.local/api/profile

Purpose: this sends a request with an untrusted origin and inspects the Access-Control-Allow-Origin and Access-Control-Allow-Credentials headers in the response. If the server reflects arbitrary origins while also allowing credentials, an attacker-controlled page could read authenticated data cross-origin — a serious and often overlooked access control failure.

Testing for Race Condition-Based Access Control Bypass

An access control check can be logically correct but still fail under concurrent requests if it’s not implemented atomically. I test this by sending multiple identical requests simultaneously against an action that should only be permittable once or under specific state conditions:

for i in {1..20}; do
  curl -s -X POST https://lab.local/api/redeem-coupon \
    -H "Authorization: Bearer <token>" \
    -d '{"couponId":"WELCOME10"}' &
done
wait

Purpose: this fires 20 simultaneous redemption requests. If the application checks “has this coupon been redeemed” and then updates the redemption status as two separate, non-atomic steps, a race condition can allow the coupon to be redeemed multiple times before the first check’s result is written back — a business-logic access control failure that sequential testing alone would never catch. The same pattern applies to testing single-use invite links, one-time discount codes, and account-linking flows.

I also use Burp Suite’s Turbo Intruder extension for this specifically, since it can fire requests with much tighter timing precision than a shell loop, which matters for exploiting narrow race windows.

Step 7: Test Access Control Across API Versions

Applications that have evolved over time frequently retain older API versions for backward compatibility, and those older versions sometimes implement authorization differently — or not at all.

GE T /api/v1/users/1042/full-profile HTTP/1.1
Authorization: Bearer <standard_user_token>

Purpose: if the current /api/v2/ endpoint properly restricts full profile access to admins but the legacy /api/v1/ equivalent was never updated with the same restriction, this reveals a backward-compatibility gap that’s easy to miss if testing focuses only on the documented, current API surface. I always check JavaScript bundles, archived API documentation, and even Wayback Machine snapshots (when in scope) for hints of deprecated endpoints that might still be live.

Step 8: Test JWT and Session Claims Against Actual Permissions

Where the application uses token-based authentication, I specifically test whether claims embedded in the token (like a cached role field) are re-validated against the current database state on every request, or trusted as-is for the token’s lifetime:

Purpose: if an admin downgrades a user’s role in the database but that user’s existing token still contains the old, more privileged role claim, and the server trusts the token’s claim without re-checking current state, the user retains elevated access until the token naturally expires — a subtle but meaningful access control gap tied to how authorization state is cached.

Common Mistakes and Troubleshooting Tips

  • Only testing with predictable sequential IDs. IDOR exists with UUIDs too, if those UUIDs can be harvested from elsewhere in the application.
  • Testing only GE-T requests. State-changing methods (POST, PUT, DELETE, PATCH) need the same scrutiny, and access control gaps often differ by method.
  • Relying on the UI to define what’s testable. Hidden or “removed” frontend features frequently still have live backend endpoints — check JavaScript source and old API documentation.
  • Forgetting about API versioning. Older API versions (/api/v1/ vs /api/v2/) sometimes retain outdated, less-restrictive access control logic.
  • Not testing role transitions carefully. Downgrading from admin to standard user mid-session (if the app allows account switching) can sometimes reveal stale authorization tokens that retain elevated privileges.
  • Manually testing every single endpoint one by one. This doesn’t scale — use Autorize or Auth Analyzer to automate the repetitive comparison across roles.

Security Risks and Defensive Recommendations

For development teams, closing these gaps consistently comes down to a few core principles:

  • Enforce authorization server-side, on every request, never relying on hiding UI elements as a security control.
  • Centralize access control logic in middleware or a policy engine rather than scattering role checks across individual route handlers, which is where gaps get introduced.
  • Verify object ownership explicitly, not just authentication — every request touching a specific resource should confirm the authenticated user is actually permitted to access that specific object.
  • Use allowlists for mass-assignment-prone endpoints, explicitly defining which fields a client is permitted to set, rather than binding entire request bodies to database models.
  • Apply the same authorization checks across all HTTP methods for a given resource path.
  • Configure CORS conservatively — avoid reflecting arbitrary origins, especially in combination with Access-Control-Allow-Credentials: true.
  • Log and monitor authorization failures, since a spike in 403 responses from a single account can indicate active exploitation attempts.

Beyond automated matrix testing, I also make time for manual “creative” testing — trying unexpected combinations like accessing an endpoint with no Authorization header at all, an expired token, a malformed token, or a token from a completely different, unrelated tenant in multi-tenant applications. Access control implementations that handle the “standard” wrong-role case correctly sometimes fall apart entirely on these edge cases, since developers often test the primary authorization path much more thoroughly than these secondary failure modes.

Building Test Matrices for Multi-Role Applications

For applications with more than two or three roles, I build an explicit test matrix before diving into manual testing — rows representing every discovered endpoint, columns representing every role, and cells marking expected versus actual behavior. This might feel like overhead for a small application, but for anything with five or more distinct permission levels (common in B2B SaaS products with organization admins, team leads, standard members, and guest collaborators), it prevents the common failure mode of testing a handful of high-profile endpoints thoroughly while leaving dozens of secondary ones completely unchecked. I generate the endpoint list from Burp’s site map after crawling as the highest-privilege role, then systematically replay each request with every lower-privilege token using Autorize, marking discrepancies for manual follow-up rather than assuming automation alone tells the full story.

Frequently Asked Questions

What’s the difference between IDOR and broken access control generally? IDOR is a specific, common type of broken access control involving direct manipulation of object identifiers; broken access control is the broader category that also includes privilege escalation, missing function-level checks, and CORS misconfigurations.

Can automated scanners reliably find access control vulnerabilities? Not fully. Scanners can flag some obvious cases, but most access control bugs require understanding the application’s intended business logic and multiple authenticated roles, which is inherently a manual testing process, even with tools like Autorize automating the repetitive parts.

Is hiding admin links in the frontend enough security? No. This is “security through obscurity” and doesn’t constitute real access control — server-side authorization checks on every request are the only reliable defense.

How do I test access control without multiple real user accounts? Set up your own lab application with distinct roles, or use platforms like PortSwigger’s Web Security Academy, which provide pre-configured multi-role scenarios specifically for this kind of practice.

What is mass assignment and how does it relate to access control? Mass assignment occurs when an application binds all fields from a request body directly to a data model without restriction, potentially allowing an attacker to set privileged fields (like role or isAdmin) that were never meant to be user-controllable.

Why is CORS misconfiguration considered an access control issue? Because overly permissive CORS policies can allow an untrusted origin to make authenticated cross-origin requests and read sensitive response data, effectively bypassing the same-origin policy that’s meant to isolate different websites from each other.

Does JWT-based authentication prevent broken access control? No — JWTs handle authentication (proving identity) but don’t inherently enforce authorization (what that identity is allowed to do). Access control logic still has to be implemented and enforced separately on every protected endpoint.

Conclusion

Broken access control persists as the most-reported risk category precisely because it’s conceptually simple but easy to get wrong in practice — every single endpoint, every HTTP method, and every object reference needs its own explicit authorization check, and it only takes one gap for the whole system to leak data or allow unauthorized actions. A methodical approach — mapping roles first, then systematically testing IDOR, privilege escalation, function-level access, and CORS configuration — turns what can feel like an overwhelming surface area into a repeatable, thorough process. Build the habit of asking “who’s actually allowed to do this, and is that actually being checked?” for every single request, and you’ll catch the majority of access control bugs before anyone else does.

For related methodology on mapping application attack surface before diving into access control testing, see my guides on mastering web hacking reconnaissance and detecting web application firewalls.

References

  • OWASP Top 10, A01:2021 Broken Access Control
  • PortSwigger Web Security Academy, Access control vulnerabilities
  • OWASP Testing Guide, Authorization Testing
Total
0
Shares

Leave a Reply

Previous Post
JWT Security Testing: Common Vulnerabilities and Test Cases

JWT Security Testing: Common Vulnerabilities and Test Cases

Next Post
Server-Side Request Forgery: SSRF Testing Methodology

Server-Side Request Forgery: SSRF Testing Methodology

Related Posts