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.
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:
Fix:
- Never put sensitive paths or logic in client-side JS
- JS minification/obfuscation is not security
4. Client-Side Enforcement Bypasses
4.1 Role via a Cookie Parameter
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:
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.
The front-end sees / → no block. The back-end sees the header → serves /admin.
Pass query parameters for actions via the normal URL:
Test method:
- Try
/admindirectly → blocked GET /+X-Original-URL: /invalid→ "not found" from the back-end? The header is being processedX-Original-URL: /admin→ access
Similar headers
X-Rewrite-URL, X-Forwarded-URL
Fix:
- Strip
X-Original-URLfrom 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,Originor other client-controllable headers
6. HTTP Method Bypass
The access control check only exists on POST requests:
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:
- Log in as admin, run through the entire flow, intercept every request in Burp
- Log in as a regular user, copy your session cookie into the step-2 request
- 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
Testing:
- Create two accounts
- Log in as account A, change the
idparameter 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:
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