Skip to main content

Free 30-min security demo  — We'll scan your real code and show live findings, no commitment Book Now

Offensive360
Academy IDOR & Broken Access Control
Intermediate · 20 min

IDOR & Broken Access Control

See how object reference vulnerabilities let attackers access anyone's data — and why server-side authorization checks are non-negotiable.

1 What is IDOR?

Insecure Direct Object Reference (IDOR) occurs when an application uses user-controllable input to directly access objects (database records, files, accounts) without verifying that the requester is authorized to access that specific object.

Classic example — order lookup with no authorization check:

@app.route('/api/orders/<int:order_id>')
@login_required
def get_order(order_id):
    # BUG: only checks that the user is logged in, not that they OWN this order
    order = Order.query.get_or_404(order_id)
    return jsonify(order.to_dict())

An attacker who owns order #1001 simply increments the ID to #1002, #1003, etc. to read every other customer's order. The application authenticates the user but does not authorize access to the specific resource.

IDOR is ranked #1 in the OWASP Top 10 as "Broken Access Control". It appears in APIs, file downloads, profile pages, admin endpoints, and anywhere a reference to a backend resource is exposed to the client.

Common IDOR patterns include: sequential integer IDs in URLs, GUIDs in query strings, filenames in parameters, and account numbers in POST bodies.

2 Horizontal vs Vertical Privilege Escalation

Access control flaws fall into two categories:

Horizontal privilege escalation — accessing another user's data at the same privilege level. Same role, different identity.

GET /api/profile?user_id=42     ← attacker is user 41, reads user 42's profile
GET /api/invoices/9887           ← attacker downloads another company's invoice
GET /api/messages/thread/550     ← attacker reads someone else's private messages

Vertical privilege escalation — accessing functionality or data reserved for a higher-privilege role.

GET /admin/users                 ← regular user hits admin endpoint (no role check)
DELETE /api/users/99             ← regular user deletes another account
POST /api/settings/global        ← regular user changes system-wide config

Using UUIDs instead of sequential IDs? UUIDs make IDs harder to guess but do not fix IDOR. Authorization must be enforced on the server. An attacker who obtains a UUID by other means (shared links, API responses, logs) can still exploit a missing ownership check.

Role-Based Access Control (RBAC) addresses vertical escalation by assigning users to roles (admin, manager, user) and checking the role before serving privileged operations.

3 Fixing with Authorization Checks

Every endpoint that accesses a user-specific resource must verify that the authenticated user is allowed to access that specific resource. Authentication (who are you?) and authorization (are you allowed to do this?) are different.

Correct ownership check on an order endpoint:

from flask import abort
from flask_login import current_user

@app.route('/api/orders/<int:order_id>')
@login_required
def get_order(order_id):
    order = Order.query.get_or_404(order_id)

    # Authorization check: does this order belong to the requesting user?
    if order.user_id != current_user.id:
        abort(403)  # Forbidden — do not reveal that the resource exists

    return jsonify(order.to_dict())

Pattern: fetch by both ID and owner in one query — simpler and avoids a separate check:

@app.route('/api/orders/<int:order_id>')
@login_required
def get_order(order_id):
    # Filters by BOTH order_id AND the current user — no extra check needed
    order = Order.query.filter_by(
        id=order_id,
        user_id=current_user.id
    ).first_or_404()
    return jsonify(order.to_dict())

Admin endpoint — role check:

@app.route('/admin/users')
@login_required
def list_all_users():
    if current_user.role != 'admin':
        abort(403)
    return jsonify([u.to_dict() for u in User.query.all()])

Key principles: Always enforce access control on the server — client-side checks (hiding buttons, not rendering links) are trivially bypassed. Return 403 (not 404) to avoid disclosing resource existence through timing. Log and alert on repeated 403s from the same IP — it may indicate an enumeration attack.

Knowledge Check

0/4 correct
Q1

A logged-in user changes /api/profile?user_id=42 to /api/profile?user_id=43 and reads another user's profile. What is this vulnerability called?

Q2

Why does replacing sequential IDs with UUIDs NOT fully fix IDOR?

Q3

What is the difference between horizontal and vertical privilege escalation?

Q4

When an authorization check fails, which HTTP status code should the server return and why?

Code Exercise

Add Ownership Check to Order Endpoint

The Flask route below fetches an order by ID but only checks that the user is logged in — any authenticated user can read any order. Add an ownership check so that only the order's owner can access it.

python