Vous subissez une attaque ? Faire appel à notre réponse aux incidents

[email protected]
Sécurité web

Testez chaque ID de votre application contre IDOR et BOLA

Repérez les endpoints qui renvoient les données d'autrui quand l'ID change, corrigez-les par des requêtes scopées et contrôlez-les en revue de code.

Aussi en :EnglishTürkçeDeutschEspañolItalianoPolskiNederlandsEesti

Illustration : Testez chaque ID de votre application contre IDOR et BOLA

Une référence directe non sécurisée à un objet (IDOR, pour insecure direct object reference) existe partout où votre application accepte un identifiant venu du client et renvoie ou modifie l'enregistrement correspondant sans se demander si cet utilisateur a le droit d'y toucher. Dans le monde des API, la même faille s'appelle broken object-level authorisation (BOLA), et elle occupe la première place du classement OWASP API Security 2023. Pour la prévenir, il faut un contrôle d'autorisation à chaque accès à un objet, appliqué à l'ensemble de l'application et couvert par des tests.

De quoi se compose une IDOR, et où elle se cache

La fiche OWASP sur les IDOR décompose une IDOR en trois éléments : un objet (un compte, un document, un ticket, une transaction), une référence à cet objet (un ID, un UUID, un numéro de compte, un jeton ou un slug) et l'absence de contrôle au niveau de l'objet. Ce troisième élément est la faille.

La référence n'est pas toujours de la forme /users/123 dans l'URL. Elle peut se trouver dans un champ de formulaire caché comme user_id, dans une query string, dans un en-tête de requête ou dans un payload JSON. Ce peut aussi être un nom de fichier : si GET /documents/annual-report.pdf fonctionne, essayez un autre nom de document valide.

L'entrée OWASP API Security consacrée à la BOLA évalue l'exploitabilité comme facile, la prévalence comme très répandue et la détectabilité comme facile. Les serveurs d'API ne suivent généralement pas l'état du client : ils s'appuient donc sur les ID envoyés par le client pour décider quel objet charger. Voici ses scénarios d'exemple :

  • Un endpoint d'analytique de boutique de la forme /shops/{shopName}/revenue_data.json, combiné à un second endpoint qui liste tous les noms de boutiques, expose les données de ventes de milliers de magasins à un simple script.
  • Une API de véhicules qui accepte un VIN mais ne vérifie jamais que le véhicule appartient à l'utilisateur connecté.
  • Une mutation GraphQL deleteReports qui supprime n'importe quel ID de document reçu.

Le dernier cas est une suppression : testez donc les mises à jour et les suppressions avec autant de soin que la page qui affiche l'enregistrement.

Testez avec deux comptes

Créez un utilisateur A et un utilisateur B, donnez à chacun ses propres objets (documents, tickets, factures), authentifiez-vous en tant que A et rejouez les requêtes avec les identifiants de B.

  1. Créez deux comptes avec des périmètres d'autorisation différents.
  2. Créez des objets appartenant à chacun des utilisateurs.
  3. Connectez-vous en tant qu'utilisateur A et capturez chaque requête qui porte une référence à un objet.
  4. Remplacez ces références par celles de l'utilisateur B dans le chemin, la query string, le corps et les en-têtes.
  5. Vérifiez que l'application refuse l'accès à chaque fois.

Couvrez la lecture, la création, la modification, la suppression, l'export et les actions d'administration. Ne passez pas un endpoint sous prétexte que ses identifiants semblent aléatoires. La fiche OWASP précise que l'accès doit être refusé que l'identifiant soit prévisible ou impossible à deviner.

Transformez-le en test de non-régression

Une fois la passe manuelle au point, transformez-la en code. Voici une ébauche en pytest, qui suppose des fixtures fournissant une session connectée pour l'utilisateur A et l'ID d'un document appartenant à B. Chaque requête doit être valide par ailleurs, avec un corps bien formé, afin qu'un 400 ou un 415 issu de la validation des entrées ne puisse pas faire passer le test pour de mauvaises raisons :

import os
import pytest

BASE_URL = os.environ["APP_BASE_URL"]  # from your test config
DENIED = 404  # your chosen response for foreign IDs

ENDPOINTS = [
    ("GET", "/documents/{id}", None),
    ("PUT", "/documents/{id}", {"title": "changed"}),
    ("DELETE", "/documents/{id}", None),
    ("GET", "/documents/{id}/export", None),
    ("POST", "/documents/{id}/comments", {"text": "hello"}),
]

@pytest.mark.parametrize("method,path,body", ENDPOINTS)
def test_a_cannot_touch_bs_document(method, path, body, session_a, b_document_id):
    url = BASE_URL + path.format(id=b_document_id)
    r = session_a.request(method, url, json=body)
    assert r.status_code == DENIED

Choisissez délibérément entre 403 et 404. Un 403 pour un ID étranger confirme que cet ID existe, ce qui aide un attaquant à énumérer. Quel que soit votre choix, n'acceptez dans le test que ce code-là. Générez la liste des endpoints à partir de votre table de routes pour que les nouveaux endpoints apparaissent automatiquement, et bloquez les déploiements lorsque la suite de tests échoue.

GraphQL demande deux cas supplémentaires. Interrogez un ID étranger à chaque niveau de l'arbre pour voir si les resolvers imbriqués répètent le contrôle du parent. Pour les opérations par lots comme le tableau reportKeys mentionné plus haut, envoyez une requête qui mélange vos propres ID et ceux d'un autre utilisateur, puis vérifiez que l'appel entier échoue ou que l'élément étranger est ignoré.

Limitez la recherche à l'utilisateur

Vérifiez l'autorisation pour chaque objet auquel un utilisateur tente d'accéder, avec le mécanisme que fournit votre framework, afin que le contrôle soit structurel et ne dépende pas de la mémoire de chaque développeur. Le schéma le plus direct consiste à rechercher les objets à l'intérieur de l'ensemble auquel l'utilisateur courant a accès. La version Rails :

# vulnerable, searches all projects
@project = Project.find(params[:id])

# secure, searches projects related to the current user
@project = @current_user.projects.find(params[:id])

Le handler Spring Boot de la fiche OWASP appelle findById(id) et renvoie donc n'importe quel document à quiconque peut deviner ou obtenir un ID. L'extrait ci-dessous est notre propre illustration, pas du code OWASP. Il suppose une requête dérivée Spring Data sur un champ ownerId, et currentUser() désigne votre helper qui lit le contexte d'authentification de confiance :

@GetMapping("/documents/{id}")
public Document getDocument(@PathVariable Long id) {
    return documentRepository
        .findByIdAndOwnerId(id, currentUser().getId())
        .orElseThrow();
}

Dans la mesure du possible, évitez d'exposer des identifiants et déduisez l'utilisateur de la session. Dans les parcours en plusieurs étapes, conservez les identifiants dans la session pour que le client ne puisse pas les altérer entre deux étapes.

Les identifiants aléatoires aident, mais seulement en défense en profondeur. Ajoutez une colonne de chaîne aléatoire et utilisez-la dans les URL, ou employez des UUID comme clés primaires. Ne chiffrez pas les ID pour les masquer : le faire de façon sûre est difficile.

Ce qui relève de la revue de code

Lisez chaque handler qui reçoit un ID fourni par le client et repérez où se fait le contrôle des droits. Regardez d'abord si la requête s'exécute sur un ensemble limité à l'utilisateur ou si elle appelle un find global et ne vérifie qu'après, si tant est qu'elle vérifie. Confirmez ensuite que le contrôle existe dans chaque fonction qui atteint un enregistrement à partir d'une entrée client, y compris la modification, la suppression et l'export.

Méfiez-vous d'un handler qui se contente de comparer l'ID de l'utilisateur de session (par exemple extrait du JWT, que vous pouvez inspecter avec notre outil JWT) avec le paramètre d'ID. Selon l'OWASP, cela ne couvre qu'un petit sous-ensemble de cas. L'autorisation doit suivre les politiques et la hiérarchie des utilisateurs. Dans un système multi-tenant, un administrateur du tenant X peut demander une facture qui appartient au tenant Y. L'utilisateur est authentifié, son rôle est administrateur et l'ID est valide : un simple contrôle de rôle peut donc passer alors que l'objet se trouve dans le mauvais tenant.

Lors du tri, distinguez bien deux failles. Dans une BOLA, l'utilisateur a le droit d'appeler l'endpoint par conception, et la violation vient de l'ID manipulé. Si un utilisateur atteint un endpoint qu'il ne devrait pas pouvoir atteindre du tout, il s'agit de broken function-level authorisation, un autre correctif.

Par où commencer

  1. Inventoriez chaque endpoint, formulaire et opération GraphQL qui accepte un identifiant, y compris les noms de fichiers et les champs cachés. Partez de votre spécification OpenAPI ou de votre table de routes, puis ajoutez ce qui n'apparaît que dans l'historique du proxy lorsque vous parcourez l'application.
  2. Lancez d'abord la passe à deux utilisateurs décrite plus haut sur les dix objets les plus sensibles, comme les factures, les données personnelles et les actions d'administration, et consignez chaque réponse qui n'est pas un refus.
  3. Remplacez les recherches globales par des recherches limitées à l'utilisateur, en vous appuyant sur le mécanisme d'autorisation de votre framework.
  4. Intégrez les tests paramétrés inter-utilisateurs à votre pipeline et faites en sorte qu'un échec bloque la mise en production.
  5. Ajoutez les questions de revue à votre modèle de pull request. Pour un contrôle externe de ce que vos tests ont manqué, vous pouvez commander notre service de test d'intrusion.

Comment nous pouvons vous aider

Tests d’intrusion

Nous attaquons vos réseaux, vos applications et vos équipes comme le ferait un véritable adversaire, puis nous vous montrons comment combler chaque faille.

Décodeur JWT

Décodez un JSON Web Token et repérez les erreurs de sécurité courantes, sans qu’il quitte votre navigateur.

Nous contacter
Équipe AngelForce

Rédigé par l’équipe AngelForce : des consultants en sécurité certifiés CEH, LPT Master et CISA qui testent, surveillent et protègent les systèmes d’organisations dans toute l’Europe et en Turquie.

Rédigé avec l’aide de l’IA à partir des sources publiques indiquées sur cette page, puis relu par l’équipe AngelForce avant publication.

Sources

  1. cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html
  2. api-security.owasp.org/editions/2023/en/0xa1-broken-object-level-authorization/

Articles : Sécurité web

Dites-nous ce que vous devez protéger

Décrivez votre environnement et vos objectifs. Nous vous répondrons par e-mail.

Nous contacter
[email protected]

AngelForce OÜ · Tallinn, Estonie