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 »
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.
- Filtrer rapidement à faible coût (vérifications rapides)
- 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 »
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.
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 ?
- 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 souventseek(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
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
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
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
sizesans appelerload()ouverify()
3ème étape : le meilleur nettoyage est « recréer » — Transcodage pour le nettoyage
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)
Le code ci‑dessous incarne la philosophie « ne lisez pas trop en validation, recréez en stockage ».
f.sizeest 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)
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 »
L'équilibre clé réside ici.
Ne soyez pas trop gourmand en validation
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
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 !

- 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é
- Ne chargez pas le fichier complet en mémoire.
read()uniquement le début, le reste en streaming. - Ne faites pas confiance à l'extension/Content‑Type ; vérifiez le MIME via Magic Number.
- En validation, ne décodez pas les pixels ; vérifiez uniquement la résolution.
- Ne sauvegardez pas l'original ; utilisez le transcodage pour créer un nouveau fichier.
- 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
Aucun commentaire.