97 lines
4.0 KiB
Markdown
97 lines
4.0 KiB
Markdown
# 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")})
|
|
``` |