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.
Cartographier l’API via le Swagger
Section intitulée « Cartographier l’API via le Swagger »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.
Le scénario IDOR
Section intitulée « Le scénario IDOR »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.
Comprendre la faille IDOR
Section intitulée « Comprendre la faille 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
idsé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.
Exploiter avec Burp
Section intitulée « Exploiter avec Burp »L’exploitation consiste à intercepter une requête légitime, puis à la rejouer en substituant l’identifiant.
- 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.1Host: api.exemple.frCookie: session=<votre_cookie_de_session>- Envoyer la requête dans Repeater (
Ctrl+R), puis modifier l’identifiant en gardant son propre cookie :
GET /api/mon-rib/1241 HTTP/1.1Host: api.exemple.frCookie: session=<votre_cookie_de_session>- Observer la réponse. Si elle renvoie les données rattachées à
1241alors 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.
Remédiation
Section intitulée « Remédiation »Vérifier la propriété de la ressource
Section intitulée « Vérifier la propriété de la ressource »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_requireddef 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_requireddef 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_requireddef get_my_rib(rib_id=None): return jsonify(Rib.query.filter_by(owner_id=current_user.id).first().to_dict())Rendre les identifiants non énumérables
Section intitulée « Rendre les identifiants non énumérables »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.
À retenir
Section intitulée « À retenir »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.