Aller au contenu

IDOR : rejouer une requête d'API pour accéder aux données d'un autre utilisateur

Le contrôle d’accès défaillant figure en tête de l’OWASP Top 10. Sa forme la plus courante sur une API : une route identifie bien qui vous êtes, mais oublie de vérifier que la ressource demandée vous appartient. Il suffit alors de changer un identifiant dans la requête pour lire les données d’un autre.

Quand une documentation Swagger (OpenAPI) est publique, elle liste les endpoints et leurs paramètres. On y repère plusieurs familles :

  • des routes d’inscription et de connexion, pour obtenir une session ;
  • des routes qui manipulent nos propres données ;
  • parfois des routes qui touchent à des informations d’autres utilisateurs, voire d’administrateurs.

Cette lecture donne la carte des points d’entrée et, surtout, la forme exacte des paramètres attendus. Une route paramétrée par un identifiant, du type /api/mon-rib/<id>, est un candidat immédiat.

Après création d’un compte et connexion, le serveur renvoie un cookie de session qui authentifie chaque requête suivante. Une route comme /api/mon-rib/<id> renvoie les informations bancaires liées à <id>.

Le problème : si la sécurité est mal faite, le serveur se contente de vérifier que la session est valide, sans vérifier que l’<id> demandé correspond bien au propriétaire de la session. Cet écart, c’est l’IDOR.

IDOR, pour Insecure Direct Object Reference, désigne une référence directe à un objet interne, ici un identifiant, exposée au client sans contrôle d’accès associé.

Le mécanisme tient en deux constats :

  • L’application authentifie, mais n’autorise pas. Elle vérifie « qui es-tu » (session valide) mais pas « as-tu le droit d’accéder à cet objet précis ». Authentification et autorisation sont deux étapes distinctes, et l’IDOR naît de la seconde manquante.
  • L’identifiant est prévisible ou énumérable. Un id séquentiel (1240, 1241, 1242) se devine. Il suffit d’incrémenter ou de substituer une valeur pour désigner l’objet d’un autre utilisateur.

Quand ces deux conditions se rencontrent, changer l’identifiant dans une requête authentifiée donne accès à des données qui ne nous appartiennent pas. L’IDOR est un contrôle d’accès horizontal défaillant : on reste au même niveau de privilège, mais on franchit la frontière entre utilisateurs.

L’exploitation consiste à intercepter une requête légitime, puis à la rejouer en substituant l’identifiant.

  1. Configurer Burp Suite comme proxy et parcourir l’application normalement, en consultant sa propre fiche pour capturer une requête valide :
GET /api/mon-rib/1240 HTTP/1.1
Host: api.exemple.fr
Cookie: session=<votre_cookie_de_session>
  1. Envoyer la requête dans Repeater (Ctrl+R), puis modifier l’identifiant en gardant son propre cookie :
GET /api/mon-rib/1241 HTTP/1.1
Host: api.exemple.fr
Cookie: session=<votre_cookie_de_session>
  1. Observer la réponse. Si elle renvoie les données rattachées à 1241 alors que cet identifiant appartient à un autre compte, l’IDOR est confirmée.

Pour mesurer l’ampleur, Intruder automatise le parcours d’une plage d’identifiants et repère les réponses 200 porteuses de données. Une énumération complète révèle alors l’intégralité des enregistrements accessibles.

Le correctif de fond : à chaque accès, confronter le propriétaire de l’objet à l’utilisateur de la session, et refuser sinon.

# Vulnérable : l'id vient du client, aucune vérification de propriété
@app.route("/api/mon-rib/<int:rib_id>")
@login_required
def get_rib(rib_id):
return jsonify(Rib.query.get(rib_id).to_dict())
# Correct : la ressource est cherchée dans le périmètre de l'utilisateur courant
@app.route("/api/mon-rib/<int:rib_id>")
@login_required
def get_rib(rib_id):
rib = Rib.query.filter_by(id=rib_id, owner_id=current_user.id).first()
if rib is None:
abort(404)
return jsonify(rib.to_dict())

Déduire l’identifiant de la session plutôt que de l’URL

Section intitulée « Déduire l’identifiant de la session plutôt que de l’URL »

Quand une route ne sert que les données de l’appelant, l’identifiant n’a pas à figurer dans l’URL. Le déduire de la session supprime la référence directe.

# La ressource est toujours celle de l'utilisateur connecté
@app.route("/api/mon-rib")
@login_required
def get_my_rib(rib_id=None):
return jsonify(Rib.query.filter_by(owner_id=current_user.id).first().to_dict())

Remplacer les entiers séquentiels par des UUID complique l’énumération. C’est une défense en profondeur, pas un substitut au contrôle de propriété : un UUID divulgué reste accessible s’il n’y a aucune vérification derrière.

L’IDOR n’exige aucune sophistication : une session valide, un identifiant prévisible dans l’URL, et une application qui authentifie sans autoriser. Le Swagger public fournit la carte, Burp rejoue la requête avec l’identifiant d’un autre, et les données tombent. La seule vraie parade est de vérifier, à chaque accès, que la ressource demandée appartient bien à l’utilisateur courant. L’obfuscation des identifiants ralentit l’attaque, elle ne la ferme pas.