# Exo 5.6 "Audit Capstone" ## Le code à auditer ```python # audit/views.py (API VOLONTAIREMENT VULNÉRABLE — à auditer) import requests from rest_framework import viewsets, serializers from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import AllowAny from .models import Equipement class EquipementSerializer(serializers.ModelSerializer): class Meta: model = Equipement fields = "__all__" class EquipementViewSet(viewsets.ModelViewSet): queryset = Equipement.objects.all() serializer_class = EquipementSerializer permission_classes = [AllowAny] @action(detail=False, methods=["post"]) def tester_webhook(self, request): url = request.data["url"] reponse = requests.get(url) return Response({"status": reponse.status_code, "corps": reponse.text}) ``` ## Constats ### 1. "fields" ouvert à tous les champs [API3] L'usage de `fields = "__all__"` permet une récupération dynamique de tous les champs de l'élément, y compris les champs éventuellement sensibles. #### Correction Usage d'un `field = ["champ1", "champ2", "champ3"]` selon les informations souhaitées. ### 2. queryset trop permissif [API1] `queryset = Equipement.objects.all()` permet la récupération de tous les objets d'une table, sans limite de propriété ou d'état connu. #### Correction Définir une fonction `def get_queryset(self):` permettant de trier selon une propriété ciblée des objets itérables par l'utilisateur, par exemple s'il est propriétaire avec `return Equipement.objects.filter(owner=self.request.user)` ### 3. permission_classes trop permissif [API5] `permission_classes = [AllowAny]` permet une connexion et des actions depuis n'importe quel compte, y compris les non-authentifiés. #### Correction Utiliser des permissions restrictives comme `permission_classes = [isAuthenticated]` voire des classes "isOwner" récupérant dynamiquement les infos. ### 4. url = request.data trop ouvert [API7] Le bloc : ``` url = request.data["url"] reponse = requests.get(url) ``` Permet à n'importe quelle source d'imposer une requête, avec Falsification des Requêtes Côté-Serveur. Non je ne le dirais pas en anglais (parce que j'ai le droit). Par exemple (pour l'avoir déjà vu auparavant), la requête avec une URL à `http://169.254.169.254/latest/meta-data/iam/security-credentials/` peut faire afficher des crédits AWS, avec ID et clef secrète. Et on peut taper avec des noms de services genre `http://postgres:5432/` et potentiellement tomber sur une page de connexion. Pire, si on ne limite pas le type de protocole à `http://` on peut aussi bien tenter un `file:///etc/passwd`. De plus, une absence d'URL peut mener à des erreurs serveurs et un Déni de Service. #### Correction Utiliser des sources précises validées avec une fonction `_hostname_resolve` pour vérifier que le point d'entrée correspond au point de sortie, avec système de liste blanche explicite - sinon liste noire par défaut -, interdire les redirections, restreindre à HTTP et HTTPS, mettre des `timeout` et autre limite de requêtes, etc. Contrôler la présence du champ "URL" ou renvoyer une erreur avec un bloc `try: except:`. ### 5. Requêtes sans garde-fou / pas de limite [API4] `reponse = requests.get(url)` se fait sans délai précisé entre deux requêtes. L'attaque par déni de service est probable. #### Correction Ralentir avec une API externe (genre Fail2Ban) ou un `sleep()`. Possibilité de limiter avec une énumération des requêtes. ### 6. Réponse de la requête brute [API3] `reponse.text` renvoie une réponse brute, non formatée. Cela peut faire fuiter une surface d'attaque par injection de script. #### Correction Retourner une ligne de caractères fixée côté serveur avant renvoi au client. Aucun envoi direct d'une variable. Du genre : ```python contenu = reponse.raw.read(settings.WEBHOOK_MAX_RESPONSE_BYTES) return Response({"status": reponse.status_code, "corps": contenu.decode("utf-8", errors="replace")}) ```