django-access-review

--- Révision de la sécurité du contrôle d'accès Django et des IDOR. À utiliser lors de l'examen des vues Django, des viewsets DRF, des requêtes ORM ou de tout code Python/Django gérant l'autorisation des utilisateurs. Mots-clés de déclenchement : « IDOR », « contrôle d'accès », « autorisation », « permissions Django », « permissions d'objet », « isolation de tenant », « accès brisé ».

npx skills add https://github.com/getsentry/skills --skill django-access-review

<!-- 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 :

  1. Comprenez comment l'autorisation fonctionne dans CETTE base de code
  2. Posez des questions sur les flux de données spécifiques
  3. Tracez le code pour trouver où (ou si) les vérifications d'accès se produisent
  4. 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