# Django/Security Téléchargement d'images, si on accepte tout, le serveur s'effondre : guide complet pour la sécurité et l'efficacité Dans les services web, le téléchargement d'images est souvent considéré comme une fonction « toujours présente » et on le fait à la légère. Pourtant, l'endpoint de téléchargement est **le passage le plus direct par lequel des données externes entrent dans le serveur** et constitue pour un attaquant **le point le moins cher pour causer le plus de dégâts** (webshell, vulnérabilité du parseur d'images, DoS, etc.). Ce texte résume comment atteindre l'objectif apparemment contradictoire de « sécurité paranoïaque, ressources économiques » en satisfaisant les deux. L'essentiel est simple. * **Dans la phase de validation, ne lisez pas le contenu** (pas de décodage, seulement en-tête/métadonnées) * **Dans la phase de stockage, recréez le fichier** (transcodage pour le nettoyage) --- ## L'essence de la sécurité du téléchargement : « ne faites pas confiance, attendez le dernier moment » {#sec-98341e6a33e1} Dans le téléchargement, peu de choses sont fiables. * Nom de fichier : l'utilisateur le change * Extension : l'utilisateur la change * Content‑Type : le client l'envoie * Contenu du fichier : l'attaquant le crée Ainsi, la stratégie se réduit à deux axes. 1. **Filtrer rapidement à faible coût** (vérifications rapides) 2. **Le fichier final est toujours généré par le serveur** (artefact généré par le serveur) --- ## Mauvaise croyance courante : « pour bien vérifier, il faut tout lire » {#sec-f9b6b6b99eb2} Les développeurs qui veulent bien sécuriser font souvent l'erreur suivante : ils insèrent du code comme celui-ci dans la phase de validation. ```python img = Image.open(file) img.verify() # ou img.load() ``` Le problème est que cela **donne aux attaquants un paiement anticipé en ressources serveur**. ### Pourquoi c'est dangereux ? {#sec-7ed56ee48a1d} * **Bombes de décompression (Decompression Bomb)** Un fichier qui semble être quelques mégaoctets peut, après décodage, devenir des dizaines de gigaoctets. `load()` déclenche le décodage réel des pixels et peut épuiser mémoire/CPU en un instant, menant à un DoS. * **I/O inutile** `verify()` lit le fichier jusqu'au bout, ce qui engendre un coût I/O élevé, et il faut souvent `seek(0)` ou rouvrir le fichier pour la suite. **Conclusion :** ne décodez pas les pixels dans la phase de « validation ». Un contrôle basé sur l'en-tête + métadonnées + limite de résolution suffit pour la première défense. --- ## Défense en profondeur : 3 étapes {#sec-5756153c302b} Le téléchargement d'images ne peut pas être résolu par une simple instruction `if`. Il faut empiler des couches. ### 1ère étape : l'extension est un mensonge — détection MIME par Magic Number {#sec-bbad131e1332} Le nom `profile.png` n'a aucune signification. Il faut lire le **signature (Magic Number)** du fichier pour confirmer son type réel. * Ne lisez pas tout le fichier avec `read()`. **Les 1 à 2 KB du début suffisent**. * Exemple de bibliothèque : `python-magic` (basé sur libmagic) ### 2ème étape : Pillow est « ouvert mais pas encore sûr » — chargement paresseux pour limiter la résolution {#sec-f0a8fb4bda8a} `Image.open()` de Pillow ne charge pas immédiatement les pixels ; il ne parse que l'en-tête. On peut donc bloquer avant tout décodage en vérifiant la **résolution (nombre de pixels)**. * Vérifier : `width * height <= MAX_PIXELS` * Astuce : utilisez `size` sans appeler `load()` ou `verify()` ### 3ème étape : le meilleur nettoyage est « recréer » — Transcodage pour le nettoyage {#sec-398d6ae888a9} Principe fondamental. > Ne sauvegardez pas l'original. Les images peuvent contenir des métadonnées (EXIF), des profils, des espaces Slack, des pièges de parseur, etc. En extrayant uniquement les données de pixels et en les enregistrant dans un nouveau format, on élimine naturellement la plupart de ces menaces. * Recommandation : **WebP (ou AVIF/JPEG)** pour le re‑encodage serveur * Bénéfices : **Nettoyage + optimisation de la taille + politique de format cohérente** --- ## Implémentation concrète : DRF Serializer (sécurité + économie mémoire) {#sec-7b99e0ec685d} Le code ci‑dessous incarne la philosophie « ne lisez pas trop en validation, recréez en stockage ». * `f.size` est déjà connu par Django, donc on l'utilise pleinement * Magic Number : **seulement le début** * Pillow : `open()` pour **la résolution uniquement** * Stockage final : **Transcodage (WebP)** * À chaque étape, on remet le pointeur de fichier à 0 avec `seek(0)` ```python from io import BytesIO import magic from PIL import Image, ImageOps, UnidentifiedImageError from rest_framework import serializers # Politique (ajustez selon votre service) MAX_SIZE = 5 * 1024 * 1024 # 5 Mo MAX_PIXELS = 4_194_304 # 2048 * 2048 ≈ 4 MP ALLOWED_MIME = {"image/png", "image/jpeg", "image/webp"} class SecureImageUploadSerializer(serializers.Serializer): file = serializers.ImageField() def validate_file(self, f): # [1] Limite de taille : filtre le plus rapide if f.size > MAX_SIZE: raise serializers.ValidationError("Le fichier est trop volumineux.") # [2] Vérification MIME par Magic Number : méfiance envers extension/Content‑Type f.seek(0) head = f.read(2048) # seulement le début f.seek(0) mime = magic.from_buffer(head, mime=True) if mime not in ALLOWED_MIME: raise serializers.ValidationError("Format de fichier non supporté.") # [3] Limite de résolution : sans load/verify, uniquement l'en-tête try: with Image.open(f) as img: w, h = img.size if (w * h) > MAX_PIXELS: raise serializers.ValidationError("La résolution de l'image est trop élevée.") except UnidentifiedImageError: raise serializers.ValidationError("Image invalide.") except Exception: raise serializers.ValidationError("Erreur lors de la validation de l'image.") finally: f.seek(0) return f def create(self, validated_data): f = validated_data["file"] # Le fichier final est toujours généré par le serveur (Nettoyage) try: with Image.open(f) as img: # Correction de l'orientation EXIF (important pour les téléphones) img = ImageOps.exif_transpose(img) # Normalisation vers un espace couleur cohérent if img.mode not in ("RGB", "RGBA"): img = img.convert("RGB") out = BytesIO() img.save(out, format="WEBP", quality=85, method=6) out.seek(0) safe_bytes = out.getvalue() # Ici, stockez safe_bytes dans votre stockage. # - Nom de fichier aléatoire (UUID) # - Sharding de répertoire (ex. ab/cd/uuid.webp) # - En base de données, ne gardez que la clé générée par le serveur return safe_bytes except Exception: raise serializers.ValidationError("Erreur lors du traitement de l'image.") ``` --- ## Équilibrer « sécurité paranoïaque » et « économie de ressources » {#sec-7ec5087de2e9} L'équilibre clé réside ici. ### Ne soyez pas trop gourmand en validation {#sec-5c32bd66af4d} La phase de validation reçoit le plus de trafic et est facilement exploitable par un attaquant. Si vous exécutez des opérations coûteuses comme `load()`, l'attaquant peut dépenser votre budget serveur à volonté. * ✅ Limite de taille / MIME basé sur l'en-tête / Limite de résolution * ❌ Décodage de pixels / Lecture complète / Réouverture multiple ### La vraie sécurité se produit en stockage {#sec-a49dab56fd86} La validation est un filtre, le stockage est la normalisation. En générant un nouveau flux d'octets, vous simplifiez la sécurité et l'exploitation. * Unification du format → simplification de la stratégie de cache / pipeline de miniatures * Nettoyage des métadonnées → suppression de données personnelles (GPS EXIF) * Réduction de la possibilité d'injection de charge malveillante --- ## Si vous prenez en compte ces points, vous êtes encore plus complet ! {#sec-733fb04d797a} ![Image illustrant la défense contre le téléchargement malveillant](/media/editor_temp/6/60e76b29-7657-4042-8e5b-3b7b2670a4c7.png) * **Ne faites jamais confiance au nom de fichier ; générez-le côté serveur** (UUID recommandé) * **Considérez une architecture où le serveur d'applications ne reçoit pas directement le fichier** (pour les gros services, utilisez des URL pré-signées vers un stockage d'objets + vérification/transformations asynchrones) * **Limitez le temps de traitement / isolez les workers** La conversion d'images consomme du CPU. Ne bloquez pas la boucle de requête‑réponse ; déléguez à un worker/queue. * **Logs / métriques** Enregistrez les raisons de refus (MIME non conforme, résolution trop grande, taille trop grande) pour détecter rapidement les schémas d'attaque ou d'abus. --- ## Checklist de résumé {#sec-ab5708035e92} 1. **Ne chargez pas le fichier complet en mémoire**. `read()` uniquement le début, le reste en streaming. 2. **Ne faites pas confiance à l'extension/Content‑Type** ; vérifiez le MIME via Magic Number. 3. **En validation, ne décodez pas les pixels** ; vérifiez uniquement la résolution. 4. **Ne sauvegardez pas l'original** ; utilisez le transcodage pour créer un nouveau fichier. 5. **Réinitialisez le pointeur de fichier à 0 à chaque étape**. La sécurité du téléchargement commence par la méfiance envers l'utilisateur, mais la performance dépend de la compréhension du fonctionnement du système. En combinant les deux, vous sécurisez votre port d'entrée. --- **Consultez aussi ces posts** - [[Faciliter le téléchargement de fichiers sur le web] Guide complet de Dropzone.js](/ko/whitedec/2025/12/24/dropzone-js-complete-guide/)