Skip to content

Broken Access Control

Source: PortSwigger Web Security Academy Author: Johan Beysen


1. What Is Broken Access Control?

Broken Access Control occurs when an application lets users perform actions or view data they don't have permission for. It's consistently the most common vulnerability in the OWASP Top 10.

Two main types:

Type Description
Vertical BAC A lower-privileged user gains access to admin/higher functions
Horizontal BAC A user gains access to another user's data at the same privilege level

Impact:

  • Unauthorized access to admin functionality
  • Insight into other users' sensitive data
  • Account takeover
  • Data manipulation or deletion

2. Detection

Test What you're looking for
robots.txt, sitemap.xml Paths to admin or sensitive endpoints
JS source (View Source) Hardcoded paths or role checks
Cookies and headers Client-controllable role/privilege values
Swapping ID parameters Horizontal access to other accounts
Switching the HTTP method A check only on POST but not on GET
Sending step 2 of a multi-step flow directly A missing check on confirmation steps

Burp Suite

Use Burp Repeater to view raw responses, even on redirects. Burp Spider automatically picks up hardcoded paths in JS.


3. Unsecured Admin Functionality

3.1 Information Leaks via robots.txt

robots.txt instructs search engines which paths to skip. It's publicly accessible.

GET /robots.txt

User-agent: *
Disallow: /administrator-panel   ← path leaked
curl https://target.com/robots.txt

Try each Disallow path directly. No authentication? Then you've hit it.

Ironic

By "hiding" a path from search engines, you make it visible to every attacker. robots.txt is not a security measure.

Also check /sitemap.xml for similar leaks.

Fix:

  • Enforce authentication and authorization on the admin route itself
  • Put the admin panel behind a VPN or IP whitelist

3.2 Paths in JS Source

The admin path is hardcoded in client-side JavaScript — visible to everyone, regardless of the UI state.

var isAdmin = false;
if (isAdmin) {
    var adminPanelTag = document.createElement('a');
    adminPanelTag.setAttribute('href', '/admin-7hf93k');  // ← path leaked
}

Search via View Source (Ctrl+U) or DevTools → Sources:

admin   panel   dashboard   hidden   secret   manage

Fix:

  • Never put sensitive paths or logic in client-side JS
  • JS minification/obfuscation is not security

4. Client-Side Enforcement Bypasses

After login, the server sets a cookie that determines the role. The application blindly trusts that value.

Set-Cookie: Admin=false   ← sent by the server
→ Change to: Admin=true
→ Access to the admin panel obtained

Search DevTools → Application → Cookies for:

role=   admin=   isAdmin=   userType=   level=   privilege=

Core problem

Access control must never be based on client-controllable values. The client can send anything it wants — cookies, headers, body parameters.

Fix:

  • Store the role/permissions exclusively server-side, in the session
  • Session ID in the cookie, look up permissions from the database based on that session

4.2 Mass Assignment via a JSON Body

The server accepts extra JSON fields in a request that it doesn't expect:

// Normal request:
{"email": "user@example.com"}

// Attack request:
{"email": "user@example.com", "roleid": 2}

// Response:
{"username": "user", "email": "...", "roleid": 2}   ← succeeded

The server maps the JSON input directly onto an object without a whitelist of allowed fields. This is called mass assignment or auto-binding.

To recognize it: API responses contain more fields than you send (roleid, isAdmin, verified...). Take those extra fields from the response → try sending them in requests too.

Fix:

  • An explicit whitelist of allowed fields in model binding
  • Laravel: $fillable | Rails: strong parameters | Spring: @JsonIgnore

5. Header-Based Bypasses

5.1 X-Original-URL

The front-end proxy blocks /admin on the URL. The back-end supports the X-Original-URL header.

GET / HTTP/1.1
X-Original-URL: /admin

The front-end sees / → no block. The back-end sees the header → serves /admin.

Pass query parameters for actions via the normal URL:

GET /?username=target HTTP/1.1
X-Original-URL: /admin/delete

Test method:

  1. Try /admin directly → blocked
  2. GET / + X-Original-URL: /invalid → "not found" from the back-end? The header is being processed
  3. X-Original-URL: /admin → access

Similar headers

X-Rewrite-URL, X-Forwarded-URL

Fix:

  • Strip X-Original-URL from incoming external requests at the proxy
  • Implement access control at the back-end, not just at the proxy layer

5.2 Referer-Based Access Control

The server checks whether the Referer header points to an admin page:

GET /admin-roles?username=wiener&action=upgrade HTTP/1.1
Referer: https://vulnerable-site.com/admin   ← added by yourself
Cookie: session=<non-admin-session>

→ 200 OK, action performed

The Referer header is entirely client-controllable.

Header Apparent purpose Reality
Referer Indicate the previous page Freely settable by the client
X-Original-URL URL override Freely settable by the client
X-Forwarded-For Original IP behind a proxy Freely settable by the client
Cookie (role=admin) Session info Freely settable by the client

Fix:

  • Never base access control on Referer, Origin or other client-controllable headers

6. HTTP Method Bypass

The access control check only exists on POST requests:

POST /admin/promote       → 403   (check present)
GET  /admin/promote?username=wiener  → 200 OK  (no check)
# Testing all methods via ffuf
ffuf -u https://target.com/admin/promote -X FUZZ -w methods.txt

Burp tip

"Change request method" in Burp Repeater automatically moves POST body parameters to the query string when converting to GET.

Fix:

  • Bind access control to the route, not the method
  • Unknown methods → 405 Method Not Allowed

7. Multi-Step Process Bypass

Access control only on step 1, not on the confirmation step:

Step 1: POST /admin/promote          → 403   (check present)
Step 2: POST /admin/promote/confirm  → 200 OK (no check)

Core mistake

Developers assume step 2 is only reachable via step 1. But HTTP has no "state" — every request is independent.

Test method:

  1. Log in as admin, run through the entire flow, intercept every request in Burp
  2. Log in as a regular user, copy your session cookie into the step-2 request
  3. Send step 2 directly, without doing step 1

Fix:

  • Access control on every step of a multi-step flow
  • Keep server-side state: check whether step 1 was completed successfully

8. IDOR (Insecure Direct Object Reference)

The application uses a user-controlled value to directly fetch an object, without checking whether that user has access to it.

8.1 A Predictable ID

GET /my-account?id=wiener   ← your own account
GET /my-account?id=carlos   ← carlos's account

Testing:

  • Create two accounts
  • Log in as account A, change the id parameter to account B's ID
  • Burp Intruder: brute force numeric IDs

Fix:

  • Check server-side whether the logged-in user owns the requested object
  • Indirect references (a mapping table) instead of direct DB IDs

8.2 GUID / Unpredictable ID

The application uses GUIDs, but they leak via other pages:

Blog post author URL:  /blogs?userId=fe89a329-d98f-4a26-b...   ← GUID leaked
Account URL:           /my-account?id=fe89a329-d98f-4a26-b...  ← same GUID

Core principle

An unpredictable ID is not access control. Security through obscurity: if the ID leaks somewhere else, the "protection" is completely useless.

Check whether user IDs appear anywhere else in the application: blog authors, forum posts, comments, email headers.

Fix:

  • GUID ≠ access control: an authorization check is still required
  • Never leak user-specific IDs in publicly accessible content

8.3 Data Leakage in a Redirect

The server sends a 302 redirect when you request an ID that isn't yours. But the response body already contains the sensitive data — before the redirect is executed.

HTTP/1.1 302 Found
Location: /home

<body>
API key: abc123xyz...   ← the browser ignores this, Burp doesn't
</body>
Tool Behavior on a 302 redirect
Browser Ignores the body, follows the redirect
Burp Repeater Shows the full raw response, including the body

Fix:

  • Clear the response body before the redirect is sent
  • Only fetch data after a successful authorization check

8.4 Files with a Predictable Name

Files stored with an incremental or predictable number:

GET /download-transcript/3.txt   ← your own file
GET /download-transcript/1.txt   ← someone else's file
ffuf -u https://target.com/download-transcript/FUZZ.txt -w numbers.txt

Fix:

  • Store files with unpredictable names (UUID)
  • Check server-side whether the user owns the file

8.5 Password Disclosure via IDOR

The profile page returns the password in the response:

GET /my-account?id=wiener          ← your own password visible
GET /my-account?id=administrator   ← the admin's password visible

Impact

Besides data exposure, this directly leads to account takeover. Impact: maximal.

Fix:

  • Never send passwords in responses, not even masked
  • Authorization check: verify the session user = the owner of the requested account
  • Only store passwords hashed

9. Patterns & Golden Rules

The Three Recurring Threads

1. Security based on client-controllable values

Cookies, headers, query parameters, JSON body — the client controls all of this. Never trust it for access control decisions.

2. Access control at one layer, bypassable via another vector

Blocked at the front-end? Try the back-end directly. Only checked on POST? Try GET. Only step 1 secured? Send step 2 directly.

3. Sensitive data leaks through unexpected channels

Redirect bodies, JS source, robots.txt, blog author URLs, API responses — data leaks everywhere if developers aren't actively watching for it.

Summary per Technique

Technique Core lesson
robots.txt leak robots.txt is not security
JS source leak Client-side JS hides nothing
Cookie role param Never put permissions in client-controllable cookies
Mass assignment Whitelist your model binding
X-Original-URL Headers are client-controllable
Method bypass Check the route, not the method
Multi-step bypass Secure every step separately
Referer check Referer is client-controllable
IDOR simple ID Always authorize access to an object
IDOR GUID GUID ≠ access control
IDOR redirect leak Clear the body before the redirect
IDOR files Files shouldn't be directly fetchable with a predictable name
Password via IDOR Passwords must not be in responses

Golden rules

  • Never trust client-supplied input for authorization decisions
  • Implement access control on the back-end, on every route, every method, every step
  • Always check: does the logged-in user own the requested object?
  • Only fetch sensitive data after a successful authorization check