<!-- Matériel de référence basé sur la série OWASP Cheat Sheet (CC BY-SA 4.0) https://cheatsheetseries.owasp.org/ -->
Examen du contrôle d'accès Django et IDOR
Trouvez les vulnérabilités de contrôle d'accès en enquêtant sur la façon dont la base de code répond à une question :
L'utilisateur A peut-il accéder, modifier ou supprimer les données de l'utilisateur B ?
Philosophie : Investigation plutôt que correspondance de motifs
Ne scannez PAS les modèles vulnérables prédéfinis. Au lieu de cela :
- Comprenez comment l'autorisation fonctionne dans CETTE base de code
- Posez des questions sur les flux de données spécifiques
- Tracez le code pour trouver où (ou si) les vérifications d'accès se produisent
- Signalez uniquement ce que vous avez confirmé par investigation
Chaque base de code implémente l'autorisation différemment. Votre travail est de comprendre cette implémentation spécifique, puis de trouver les lacunes.
Phase 1 : Comprendre le modèle d'autorisation
Avant de chercher des bugs, répondez à ces questions sur la base de code :
Comment l'autorisation est-elle appliquée ?
Enquêtez sur la base de code pour trouver :
□ Où les vérifications de permissions sont-elles implémentées ?
- Décorateurs ? (@login_required, @permission_required, personnalisé ?)
- Middleware ? (TenantMiddleware, AuthorizationMiddleware ?)
- Classes de base ? (BaseAPIView, TenantScopedViewSet ?)
- Classes de permissions ? (permission_classes DRF ?)
- Mixins personnalisés ? (OwnershipMixin, TenantMixin ?)
□ Comment les requêtes sont-elles scoped ?
- Gestionnaires personnalisés ? (TenantManager, UserScopedManager ?)
- Remplacements de get_queryset() ?
- Middleware qui définit le contexte de requête ?
□ Quel est le modèle de propriété ?
- Propriété d'utilisateur unique ? (document.owner_id)
- Propriété d'organisation/tenant ? (document.organization_id)
- Hiérarchique ? (org -> équipe -> utilisateur -> ressource)
- Basé sur les rôles dans le contexte ? (administrateur org vs membre)
Commandes d'investigation
# Trouvez comment l'authentification est généralement effectuée
grep -rn "permission_classes\|@login_required\|@permission_required" --include="*.py" | head -20
# Trouvez les classes de base dont les vues héritent
grep -rn "class Base.*View\|class.*Mixin.*:" --include="*.py" | head -20
# Trouvez les gestionnaires personnalisés
grep -rn "class.*Manager\|def get_queryset" --include="*.py" | head -20
# Trouvez les champs de propriété sur les modèles
grep -rn "owner\|user_id\|organization\|tenant" --include="models.py" | head -30
Ne procédez pas jusqu'à ce que vous compreniez le modèle d'autorisation.
Phase 2 : Mapper la surface d'attaque
Identifiez les endpoints qui gèrent les données spécifiques à l'utilisateur :
Quelles ressources existent ?
□ Quels modèles contiennent des données utilisateur ?
□ Lesquels ont des champs de propriété (owner_id, user_id, organization_id) ?
□ Lesquels sont accessibles via un ID dans les URL ou les corps de requête ?
Quelles opérations sont exposées ?
Pour chaque ressource, cartographiez :
- Endpoints de liste - quelles données sont retournées ?
- Endpoints de détail/récupération - comment l'objet est-il récupéré ?
- Endpoints de création - qui définit le propriétaire ?
- Endpoints de mise à jour - les utilisateurs peuvent-ils modifier les données d'autres ?
- Endpoints de suppression - les utilisateurs peuvent-ils supprimer les données d'autres ?
- Actions personnalisées - qu'accèdent-elles ?
Phase 3 : Poser des questions et enquêter
Pour chaque endpoint qui gère les données utilisateur, posez :
La question centrale
« Si je suis l'utilisateur A et je connais l'ID de la ressource de l'utilisateur B, puis-je y accéder ? »
Tracez le code pour répondre :
1. Où l'ID de la ressource entre-t-il dans le système ?
- Chemin URL : /api/documents/{id}/
- Paramètre de requête : ?document_id=123
- Corps de la requête : {"document_id": 123}
2. Où cet ID est-il utilisé pour récupérer les données ?
- Trouvez la requête ORM ou l'appel à la base de données
3. Entre (1) et (2), quelles vérifications existent ?
- La requête est-elle scoped à l'utilisateur actuel ?
- Y a-t-il une vérification de propriété explicite ?
- Y a-t-il une vérification de permission sur l'objet ?
- Une classe de base ou un mixin applique-t-il l'accès ?
4. Si vous ne trouvez pas de vérification, en avez-vous manqué une ?
- Vérifiez les classes parentes
- Vérifiez le middleware
- Vérifiez les gestionnaires
- Vérifiez les décorateurs au niveau de l'URL
Questions de suivi
□ Pour les endpoints de liste : La requête est-elle filtrée aux données de l'utilisateur,
ou retourne-t-elle tout ?
□ Pour les endpoints de création : Qui définit le propriétaire - le serveur ou la requête ?
□ Pour les opérations en masse : Sont-elles scoped aux données de l'utilisateur ?
□ Pour les ressources liées : Si je peux accéder à un document, puis-je accéder
à ses commentaires ? Et si le document appartient à quelqu'un d'autre ?
□ Pour les ressources tenant/org : Un utilisateur de l'org A peut-il accéder
aux données de l'org B en changeant l'org_id dans l'URL ?
Phase 4 : Tracer les flux spécifiques
Choisissez un endpoint concret et tracez-le complètement.
Exemple d'investigation
Endpoint: GET /api/documents/{pk}/
1. Trouvez la vue qui gère cette URL
→ DocumentViewSet.retrieve() dans api/views.py
2. Vérifiez dont DocumentViewSet hérite
→ class DocumentViewSet(viewsets.ModelViewSet)
→ Pas de classe de base personnalisée avec autorisation
3. Vérifiez permission_classes
→ permission_classes = [IsAuthenticated]
→ Vérifie uniquement la connexion, pas la propriété
4. Vérifiez get_queryset()
→ def get_queryset(self):
→ return Document.objects.all()
→ Retourne TOUS les documents !
5. Vérifiez has_object_permission()
→ Non implémenté
6. Vérifiez la méthode retrieve()
→ Utilise le comportement par défaut, qui appelle get_object()
→ get_object() utilise get_queryset(), qui retourne tous
7. Conclusion : IDOR - Tout utilisateur authentifié peut accéder à tout document
Ce qu'il faut chercher lors du traçage
Indicateurs de lacune potentielle (enquêtez davantage, ne signalez pas automatiquement) :
- get_queryset() retourne .all() ou filtre sans utilisateur
- Direct Model.objects.get(pk=pk) sans propriété dans la requête
- L'ID provient du corps de la requête pour les opérations sensibles
- La classe de permissions vérifie l'authentification mais pas la propriété
- Pas de has_object_permission() et le queryset n'est pas scoped
Motifs probablement sûrs (mais vérifiez l'implémentation) :
- get_queryset() filtre par request.user ou l'org de l'utilisateur
- Classe de permissions personnalisée avec has_object_permission()
- Classe de base qui applique le scoping
- Gestionnaire qui filtre automatiquement
Phase 5 : Signaler les résultats
Signalez uniquement les problèmes que vous avez confirmés par investigation.
Niveaux de confiance
| Niveau | Signification | Action |
|---|---|---|
| HIGH | Tracé le flux, confirmé qu'aucune vérification n'existe | Signalez avec des preuves |
| MEDIUM | La vérification peut exister mais impossible de confirmer | À noter pour vérification manuelle |
| LOW | Théorique, probablement atténué | Ne signalez pas |
Les correctifs suggérés doivent appliquer, non documenter
Mauvais correctif : Ajouter un commentaire disant « l'appelant doit valider les permissions » Bon correctif : Ajouter du code qui valide réellement les permissions
Un commentaire ou une docstring n'applique pas l'autorisation. Votre correctif suggéré doit inclure du code qui :
- Valide que l'utilisateur a la permission avant de continuer
- Lève une exception ou retourne une erreur si non autorisé
- Rend l'accès non autorisé impossible, pas seulement découragé
Exemple d'une suggestion de correctif MAUVAISE :
def get_resource(resource_id):
# IMPORTANT : L'appelant doit s'assurer que l'utilisateur a accès à cette ressource
return Resource.objects.get(pk=resource_id)
Exemple d'une suggestion de correctif BONNE :
def get_resource(resource_id, user):
resource = Resource.objects.get(pk=resource_id)
if resource.owner_id != user.id:
raise PermissionDenied("Accès refusé")
return resource
Si vous ne pouvez pas déterminer le mécanisme d'application approprié, dites-le - mais ne suggérez jamais la documentation comme correctif.
Format de rapport
## Examen du contrôle d'accès : [Composant]
### Modèle d'autorisation
[Brève description de comment cette base de code gère l'autorisation]
### Résultats
#### [IDOR-001] [Titre] (Sévérité : Haute/Moyenne)
- **Localisation** : `chemin/vers/fichier.py:123`
- **Confiance** : Haute - confirmé par traçage du code
- **La question** : L'utilisateur A peut-il accéder aux documents de l'utilisateur B ?
- **Investigation** :
1. Tracé GET /api/documents/{pk}/ vers DocumentViewSet
2. Vérifié get_queryset() - retourne Document.objects.all()
3. Vérifié permission_classes - seulement IsAuthenticated
4. Vérifié has_object_permission() - non implémenté
5. Vérifié qu'aucun middleware ou contrôle de classe de base pertinent
- **Preuve** : [Fragment de code montrant la lacune]
- **Impact** : Tout utilisateur authentifié peut lire tout document par ID
- **Correctif suggéré** : [Code qui applique l'autorisation - PAS un commentaire]
### Nécessite vérification manuelle
[Problèmes où l'autorisation existe mais impossible de confirmer l'efficacité]
### Zones non examinées
[Endpoints ou flux non couverts dans cet examen]
Motifs d'autorisation Django courants
Ce sont des motifs que vous pourriez trouver - pas une liste de contrôle à faire correspondre.
Scoping de requête
# Scoped à l'utilisateur
Document.objects.filter(owner=request.user)
# Scoped à l'organisation
Document.objects.filter(organization=request.user.organization)
# Utilisant un gestionnaire personnalisé
Document.objects.for_user(request.user) # Enquêtez sur ce que cela fait
Application de permissions
# Classes de permissions DRF
permission_classes = [IsAuthenticated, IsOwner]
# has_object_permission personnalisé
def has_object_permission(self, request, view, obj):
return obj.owner == request.user
# Décorateurs Django
@permission_required('app.view_document')
# Vérifications manuelles
if document.owner != request.user:
raise PermissionDenied()
Attribution de propriété
# Côté serveur (sûr)
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
# À partir de la requête (enquêtez)
serializer.save(**request.data) # request.data inclut-il le propriétaire ?
Liste de contrôle d'investigation
Utilisez ceci pour guider votre examen, pas comme une liste à cocher réussite/échec :
□ Je comprends comment l'autorisation est généralement implémentée dans cette base de code
□ J'ai identifié le modèle de propriété (utilisateur, org, tenant, etc.)
□ J'ai cartographié les endpoints clés qui gèrent les données utilisateur
□ Pour chaque endpoint sensible, j'ai tracé le flux et posé :
- Où l'ID provient-il ?
- Où les données sont-elles récupérées ?
- Quelles vérifications existent entre l'entrée et l'accès aux données ?
□ J'ai vérifié mes résultats en vérifiant les classes parentes et le middleware
□ J'ai seulement signalé les problèmes que j'ai confirmés par investigation