Broken access control is the #1 web application vulnerability according to OWASP. This guide covers IDOR, privilege escalation, and forced browsing — with practical prevention strategies and real-world examples.
Anay Pandya
1,542 words
Anay Pandya
Founder of ZeriFlow · 10 years fullstack engineering · About the author
Key Takeaways
Broken access control is the most prevalent web application security vulnerability in the world, claiming the top position in the OWASP Top 10 since 2021. Unlike exotic memory corruption bugs, broken access control vulnerabilities are conceptually simple: users can access resources or perform actions they shouldn't be allowed to. The consequence ranges from data exposure to complete account takeover and administrative compromise.
<div class="zf-stat-callout" style="background:#0d1117;border:1px solid rgba(16,185,129,0.25);border-left:3px solid #10b981;border-radius:4px;padding:16px 20px;margin:24px 0"> <p style="margin:0 0 4px;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:0.15em;color:#10b981;font-family:monospace">ZeriFlow Data — 12,400+ sites analyzed</p> <p style="margin:0;font-size:13px;color:#e2e8f0;line-height:1.6;font-family:monospace">In ZeriFlow's corpus of 12,400+ scanned sites, 72% score below 70/100 on security. Only 7% achieve a score above 85 — a threshold that corresponds to passing all OWASP-aligned header and configuration checks.</p> </div>
Keep reading
Is your site actually secure?
Run a free check — 60 seconds
[Scan your web application with ZeriFlow](https://zeriflow.com) — free security assessment covering 80+ vulnerability checks.
Access control is the enforcement of rules about who can do what. A properly implemented system ensures:
Broken access control means the authorization check is missing, incomplete, or bypassable. The most common categories:
Insecure Direct Object Reference occurs when an application uses a user-controlled identifier to access an object directly, without checking whether the current user is authorized to access that object.
GET /api/invoices/1234
Authorization: Bearer alice_tokenAlice's invoice is #1234. She changes the ID:
GET /api/invoices/1235
Authorization: Bearer alice_tokenIf the server returns Bob's invoice #1235, that's IDOR. The server authenticated Alice correctly but failed to verify that invoice #1235 belongs to Alice.
# VULNERABLE
@app.route('/api/invoices/<int:invoice_id>')
@require_auth
def get_invoice(invoice_id):
invoice = Invoice.query.get(invoice_id)
if not invoice:
return jsonify({'error': 'Not found'}), 404
return jsonify(invoice.to_dict()) # No ownership check!# SECURE
@app.route('/api/invoices/<int:invoice_id>')
@require_auth
def get_invoice(invoice_id):
# Filter by BOTH id AND current user
invoice = Invoice.query.filter_by(
id=invoice_id,
user_id=current_user.id # ownership check
).first()
if not invoice:
return jsonify({'error': 'Not found'}), 404 # Same response for not found vs unauthorized
return jsonify(invoice.to_dict())Critical detail: Return 404 Not Found rather than 403 Forbidden when authorization fails for a resource the user shouldn't know exists. Returning 403 confirms the resource exists, enabling enumeration.
A regular user performs actions reserved for administrators. Common vectors:
Parameter manipulation:
POST /api/users/update
{"user_id": 123, "role": "admin"} // user tries to promote themselvesURL guessing:
GET /admin/users // Should require admin, but doesn't check
GET /admin/export-data
DELETE /admin/users/456Hidden form fields:
<input type='hidden' name='role' value='user'>
<!-- attacker changes to -->
<input type='hidden' name='role' value='admin'>A user accesses another user's data at the same privilege level — the IDOR pattern described above.
def require_role(*roles):
def decorator(f):
@wraps(f)
def decorated(*args, **kwargs):
if current_user.role not in roles:
abort(403)
return f(*args, **kwargs)
return decorated
return decorator
@app.route('/admin/users')
@require_auth
@require_role('admin', 'superadmin')
def list_users():
return jsonify(User.query.all())Never trust client-supplied role or permission values. Always derive authorization from the server-side session.
Forced browsing (also called insecure direct URL access) occurs when sensitive pages aren't properly protected by server-side access control — relying instead on obscurity (not linking to the page) rather than enforcement.
Examples:
- /admin/ accessible without authentication
- /reports/financial-2024-q4.pdf accessible by guessing the filename
- /api/v2/internal/debug exposed with no auth
- /backup/database.sql.gz sitting in the web root
# Use common wordlists to discover unprotected endpoints
ffuf -u https://target.com/FUZZ -w /usr/share/wordlists/dirb/common.txt
gobuster dir -u https://target.com -w /opt/SecLists/Discovery/Web-Content/raft-large-words.txtAutomated scanners also discover these. ZeriFlow checks for common sensitive paths and directory listing exposure as part of its 80+ checks.
JSON Web Tokens are widely misimplemented in ways that break access control:
# Attacker crafts a token with alg: 'none'
import base64, json
header = base64.b64encode(json.dumps({'alg': 'none', 'typ': 'JWT'}).encode()).decode()
payload = base64.b64encode(json.dumps({'user_id': 1, 'role': 'admin'}).encode()).decode()
malicious_token = f'{header}.{payload}.' # empty signatureIf the server accepts alg: none, it skips signature verification entirely — accepting any token as valid.
Fix: Explicitly specify and enforce the expected algorithm. Reject none.
# VULNERABLE: verifies the token is valid but doesn't check permissions
@app.route('/admin/delete-user/<int:user_id>', methods=['DELETE'])
@jwt_required() # only checks token validity, not role
def delete_user(user_id):
User.query.filter_by(id=user_id).delete()
return jsonify({'success': True})Always verify both authentication AND authorization. A valid JWT proves identity; it doesn't grant permission.
A permissive CORS policy (Access-Control-Allow-Origin: * on authenticated endpoints) can break access control by allowing cross-site JavaScript to read responses from your authenticated API:
# DANGEROUS: wildcard on authenticated endpoint
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: trueNote: * and Allow-Credentials: true are mutually exclusive per the spec — but bugs in some implementations and frameworks can make this dangerous. Always restrict Allow-Origin to specific trusted origins on authenticated endpoints.
Grant users the minimum permissions needed to do their job. Default deny — if you haven't explicitly granted access, it's denied.
Never rely on client-side checks. A user can bypass any JavaScript check with browser devtools. All authorization must be enforced on the server.
Don't scatter authorization checks throughout your codebase. Implement a central access control layer:
class AccessControl:
def can_read_invoice(self, user, invoice):
return invoice.user_id == user.id or user.role == 'admin'
def can_delete_user(self, user, target_user):
return user.role == 'admin' and user.id != target_user.id
def can_access_report(self, user, report):
return report.team_id in user.team_ids or user.role == 'admin'
ac = AccessControl()
@app.route('/invoices/<int:invoice_id>')
@require_auth
def get_invoice(invoice_id):
invoice = Invoice.query.get_or_404(invoice_id)
if not ac.can_read_invoice(current_user, invoice):
abort(404) # 404, not 403, to avoid enumeration
return jsonify(invoice.to_dict())A: Because it's extremely common (94% of tested applications had some form), the impact is high (data breaches, account takeover, full system compromise), and it's often a business logic flaw that automated scanners miss. It's not a new vulnerability type — it's the failure to consistently apply authorization checks throughout an application.
A: Create two test accounts at different privilege levels. Log in as the lower-privilege user and attempt to access resources belonging to the higher-privilege user and to other same-level users. Systematically test every API endpoint with each account. Tools like Burp Suite's Autorize extension automate this. ZeriFlow provides a first-pass scan for common access control indicators.
A: UUIDs make IDOR harder to exploit by making IDs unguessable, but they're not a fix. If an attacker obtains a UUID (via a data breach, log exposure, or enumeration), IDOR is still exploitable. Always implement proper ownership checks. Use UUIDs AND ownership checks.
A: Authentication answers "Who are you?" — verified via passwords, tokens, biometrics. Authorization answers "What are you allowed to do?" — verified via roles, permissions, ownership checks. Broken access control is almost always an authorization failure, not an authentication failure.
A: ZeriFlow scans for common access control indicators: exposed admin paths, directory listing, sensitive file exposure, CORS misconfigurations, and missing security headers that could aid privilege escalation. Run a free scan at ZeriFlow as your baseline, then follow up with manual authorization testing for complete coverage.
Broken access control is #1 on the OWASP Top 10 because it's everywhere and the consequences are severe. The pattern is always the same: the application authenticates the user but fails to verify they're authorized for the specific resource or action.
The fix is systematic: centralize your authorization logic, enforce ownership checks on every data access, default-deny everything, and test with multiple account privilege levels. [Start with a free ZeriFlow scan](https://zeriflow.com) to identify exposed paths and configuration weaknesses that make broken access control easier to exploit — then build proper server-side authorization on top.
Access control isn't glamorous. It's also the difference between a secure application and a data breach.
<!-- zf-internal-links -->
Related resources
Website Vulnerability Scanner
Run a broader website security audit across headers, TLS, DNS, cookies, SEO, and disclosure checks.
Security Headers Checker
Check CSP, HSTS, X-Frame-Options, and other response headers.
SSL Checker
Review TLS certificate, HTTPS, and transport security signals.
DMARC Checker
Validate email authentication records for domain spoofing protection.
CSP Checker
Review Content-Security-Policy coverage and common gaps.
June 24, 2026
Best Security Tools for AI Startups
Monitor your security score
Schedule recurring scans and catch regressions after deployments.