Files
gauvainboiche 0d071d2843 Semaine 11
2026-07-11 21:38:00 +02:00

4.0 KiB

Exo 5.6 "Audit Capstone"

Le code à auditer

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

contenu = reponse.raw.read(settings.WEBHOOK_MAX_RESPONSE_BYTES)
return Response({"status": reponse.status_code, "corps": contenu.decode("utf-8", errors="replace")})