Free — no signup required

A01: Broken Access Control (IDOR and Beyond)

3 min read

The #1 Vulnerability: Doing Things You Shouldn't Be Allowed To Do

Access control is the set of rules that determines what an authenticated user is allowed to do. Broken Access Control means those rules are either missing, implemented incorrectly, or only enforced on the client side (where an attacker can bypass them trivially). It ranked #1 in the 2021 OWASP Top 10 because it was found in 94% of applications tested — nearly universal.

The most intuitive form is Insecure Direct Object Reference (IDOR): the application exposes a direct reference to an internal object (a database ID, a filename, an account number), and when the user changes that reference, the server doesn't verify whether they're authorized to access the new object.

The Classic IDOR Attack

# You log in and see your invoice:
GET /api/invoices/10482

# You change the ID:
GET /api/invoices/10481

# The server returns someone else's invoice — no error, no check.

This is not a sophisticated attack. It requires no special tools, no malware, no exploit code — just changing a number in a URL. Yet it has been responsible for some of the largest data breaches in history, exposing millions of records.

The Fix: Server-Side Authorization on Every Request

The critical insight is that you cannot trust the client to enforce access control. Hiding a button in the UI, or not displaying a link to /api/invoices/10481, does nothing to prevent a direct HTTP request. Every request that accesses a resource must be validated on the server.

# VULNERABLE: Fetches the resource without checking ownership
@app.route('/api/invoices/<invoice_id>')
def get_invoice(invoice_id):
    invoice = db.find('invoices', invoice_id)
    return jsonify(invoice)

# SECURE: Checks ownership before returning data
@app.route('/api/invoices/<invoice_id>')
def get_invoice(invoice_id):
    invoice = db.find('invoices', invoice_id)

    if not invoice:
        return error("Not found"), 404

    # THE CRITICAL CHECK: Does the logged-in user own this invoice?
    if invoice.owner_id != current_user.id:
        # Return 403, not 404. (Though 404 is sometimes preferred to avoid leaking existence.)
        return error("Forbidden"), 403

    return jsonify(invoice)

Broader Broken Access Control Patterns

IDOR is one manifestation. The category also includes:

  • Privilege escalation: A regular user accessing admin endpoints (/admin/users/delete) because the server only checks if the user is authenticated, not if they have the admin role.
  • Forced browsing: Accessing URLs that aren't linked in the UI but aren't protected (e.g., /internal/reports/q4-financials.pdf).
  • CORS misconfiguration: Allowing Access-Control-Allow-Origin: * on APIs that return sensitive data, enabling any website to read the response.
  • JWT tampering: Modifying a JSON Web Token's payload (e.g., changing "role": "user" to "role": "admin") when the server doesn't properly verify the signature.

Key Point: The architectural principle here is deny by default. Every endpoint should start from a position of "access denied" and explicitly grant access only after verifying identity and authorization. Frameworks like Spring Security, Django's permission system, and AWS IAM all implement this model. If you find yourself writing "allow" logic without a corresponding "deny everything else" default, you likely have a Broken Access Control vulnerability.

This is one of 18 chapters

Every chapter is free — Kubernetes, Terraform, SRE, distributed systems, and more — with fast daily review built in.

Sign up free