Skip to main content
Back to blog
April 28, 2026|9 min read|Antoine Duno

Broken Access Control: OWASP #1 Explained with Prevention

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.

ZeriFlow Team

1,437 words

Broken Access Control: OWASP #1 Explained with Prevention

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.

[Scan your web application with ZeriFlow](https://zeriflow.com) — free security assessment covering 80+ vulnerability checks.


What Is Broken Access Control?

Access control is the enforcement of rules about who can do what. A properly implemented system ensures:

  • Authentication: You are who you say you are.
  • Authorization: You are allowed to do what you're trying to do.

Broken access control means the authorization check is missing, incomplete, or bypassable. The most common categories:

  1. 1IDOR (Insecure Direct Object Reference): Accessing another user's resource by changing an ID.
  2. 2Privilege escalation: Performing admin actions as a regular user.
  3. 3Forced browsing: Accessing pages/endpoints that require authorization by guessing URLs.
  4. 4Path traversal: Accessing files outside allowed directories (covered in our directory traversal guide).
  5. 5Method tampering: Using GET instead of POST (or vice versa) to bypass authorization.

IDOR: The Most Common Form

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.

Classic IDOR Example

GET /api/invoices/1234
Authorization: Bearer alice_token

Alice's invoice is #1234. She changes the ID:

GET /api/invoices/1235
Authorization: Bearer alice_token

If 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 Code

python
# 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 Code

python
# 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.


Privilege Escalation

Vertical Privilege Escalation

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 themselves

URL guessing:

GET /admin/users           // Should require admin, but doesn't check
GET /admin/export-data
DELETE /admin/users/456

Hidden form fields:

html
<input type='hidden' name='role' value='user'>
<!-- attacker changes to -->
<input type='hidden' name='role' value='admin'>

Horizontal Privilege Escalation

A user accesses another user's data at the same privilege level — the IDOR pattern described above.

Preventing Privilege Escalation

python
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

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

Testing for Forced Browsing

bash
# 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.txt

Automated scanners also discover these. ZeriFlow checks for common sensitive paths and directory listing exposure as part of its 80+ checks.


JWT and Token-Based Authorization Failures

JSON Web Tokens are widely misimplemented in ways that break access control:

Algorithm Confusion Attack

python
# 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 signature

If 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.

Missing Authorization Check After Authentication

python
# 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.


CORS Misconfigurations as Access Control Failures

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: true

Note: * 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.


Building a Secure Authorization Architecture

Principle of Least Privilege

Grant users the minimum permissions needed to do their job. Default deny — if you haven't explicitly granted access, it's denied.

Server-Side Policy Enforcement

Never rely on client-side checks. A user can bypass any JavaScript check with browser devtools. All authorization must be enforced on the server.

Centralize Access Control Logic

Don't scatter authorization checks throughout your codebase. Implement a central access control layer:

python
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())

FAQ

Q: Why is broken access control OWASP's #1 vulnerability?

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.

Q: How do I test my application for broken access control?

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.

Q: Is UUIDs/random IDs a sufficient fix for IDOR?

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.

Q: What's the difference between authentication and authorization?

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.

Q: How does ZeriFlow help with access control issues?

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.


Conclusion

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.

Ready to check your site?

Run a free security scan in 30 seconds.

Related articles

Keep reading