# Django/Security Subir imágenes, si no se controla, puede derribar el servidor: la guía completa que combina seguridad y eficiencia En los servicios web, la subida de imágenes suele considerarse una función “obvia” y se implementa de forma rápida. Sin embargo, el endpoint de carga es **el canal más directo por el que datos externos entran al servidor** y, desde la perspectiva de un atacante, es el punto con mayor potencial de daño al menor coste (carga de web shells, vulnerabilidades de parsers de imágenes, DoS, etc.). Esta publicación explica cómo cumplir simultáneamente los objetivos aparentemente contradictorios de **seguridad paranoica y uso racional de recursos**. La clave es sencilla: * **En la fase de validación, evita leer el contenido** (solo decodifica encabezados y metadatos). * **En la fase de almacenamiento, recrea la imagen** (transcodificación para sanitizar). --- ## La esencia de la seguridad en la carga: “No confíes, confía lo más tarde posible” {#sec-98341e6a33e1} En la carga, casi nada es confiable: * Nombre de archivo: el usuario lo cambia. * Extensión: el usuario la cambia. * Content‑Type: el cliente lo envía. * Contenido: el atacante lo crea. Por ello, la estrategia se reduce a dos pasos: 1. **Filtrar rápidamente con comprobaciones baratas** (cheap checks). 2. **El archivo final siempre lo genera el servidor** (artefacto generado por el servidor). --- ## Mitos comunes: “Para comprobar bien, hay que leer todo” {#sec-f9b6b6b99eb2} Un error frecuente es colocar código como el siguiente en la fase de validación: ```python img = Image.open(file) img.verify() # o img.load() ``` El problema es que **está entregando recursos del servidor al atacante de forma anticipada**. ### ¿Por qué es peligroso? {#sec-7ed56ee48a1d} * **Bombas de descompresión** Un archivo que parece de pocos MB puede expandirse a decenas de GB al decodificarse. `load()` desencadena la decodificación real de píxeles, agotando memoria/CPU y provocando DoS. * **I/O innecesario** `verify()` lee el archivo hasta el final, generando un coste de I/O alto; además, suele requerir `seek(0)` o re‑apertura para el siguiente paso. **Conclusión:** en la fase de validación, **no decodifiques píxeles**. Un encabezado + metadatos + límite de resolución es suficiente para la primera defensa. --- ## Defensa en profundidad: tres capas {#sec-5756153c302b} La carga de imágenes no se resuelve con una sola línea `if`. Se necesita una pila de defensas. ### 1ª capa: la extensión es una mentira — detección MIME por Magic Number {#sec-bbad131e1332} El nombre `profile.png` no significa nada. Se debe leer el **firmado (Magic Number)** del archivo para verificar su tipo real. * No leas el archivo completo. **Los primeros 1‑2 KB bastan**. * Ejemplo de librería: `python‑magic` (basado en libmagic). ### 2ª capa: Pillow es seguro al *abrir* — carga perezosa y límite de resolución {#sec-f0a8fb4bda8a} `Image.open()` de Pillow normalmente **no carga los píxeles inmediatamente**, solo analiza el encabezado. Aprovecha esto para bloquear antes de que se produzca una explosión de memoria. * Verifica: `width * height <= MAX_PIXELS`. * Punto clave: usa `size` sin `load()`/`verify()`. ### 3ª capa: la mejor sanitización es *re‑dibujar* — transcodificación {#sec-398d6ae888a9} Principio fundamental. > No guardes el original. Las imágenes pueden contener metadatos (EXIF), perfiles, trucos de parseo, etc. Al extraer solo los datos de píxeles y guardarlos en un nuevo formato, se eliminan la mayoría de los riesgos. * Recomendado: **WebP** (o AVIF/JPEG) para re‑codificar. * Beneficios: sanitización, compresión, política de formato consistente. --- ## Implementación práctica: DRF Serializer (seguridad + eficiencia de memoria) {#sec-7b99e0ec685d} El siguiente código incorpora la filosofía de “no leer en la validación, recrear en el almacenamiento”. * `f.size` es metadato que ya conoce Django. * Magic Number solo lee la cabeza. * Pillow solo verifica la resolución. * Almacenamiento final: transcodificación a WebP. * Cada paso restaura el puntero con `seek(0)`. ```python from io import BytesIO import magic from PIL import Image, ImageOps, UnidentifiedImageError from rest_framework import serializers # Políticas (ajusta a tu servicio) MAX_SIZE = 5 * 1024 * 1024 # 5 MB 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] Límite de tamaño: filtro barato y rápido if f.size > MAX_SIZE: raise serializers.ValidationError("El archivo es demasiado grande.") # [2] MIME por Magic Number: no confiar en extensión/Content‑Type f.seek(0) head = f.read(2048) f.seek(0) mime = magic.from_buffer(head, mime=True) if mime not in ALLOWED_MIME: raise serializers.ValidationError("Formato de archivo no soportado.") # [3] Límite de resolución: sin load/verify, solo encabezado try: with Image.open(f) as img: w, h = img.size if (w * h) > MAX_PIXELS: raise serializers.ValidationError("La resolución de la imagen es demasiado alta.") except UnidentifiedImageError: raise serializers.ValidationError("Imagen no válida.") except Exception: raise serializers.ValidationError("Error al validar la imagen.") finally: f.seek(0) return f def create(self, validated_data): f = validated_data["file"] # Almacenamiento final: siempre generado por el servidor (sanitizado) try: with Image.open(f) as img: # Rotación por EXIF (muy importante en móviles) img = ImageOps.exif_transpose(img) # Normalizar espacio de color 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() # Aquí guarda safe_bytes en tu almacenamiento. # - Nombra con UUID # - Sharding de directorios (ej.: ab/cd/uuid.webp) # - En la BD guarda solo la clave generada por el servidor return safe_bytes except Exception: raise serializers.ValidationError("Error al procesar la imagen.") ``` --- ## Equilibrar “seguridad paranoica” y “eficiencia de recursos” {#sec-7ec5087de2e9} El equilibrio clave es: ### No ser codicioso en la validación {#sec-5c32bd66af4d} La validación es el punto de mayor tráfico y el que el atacante puede invocar repetidamente sin coste. Si ejecutas `load()` u operaciones costosas, el atacante puede agotar recursos del servidor. * ✅ Límite de tamaño / MIME por encabezado / límite de resolución * ❌ Decodificación de píxeles / lectura completa / re‑apertura múltiple ### La verdadera seguridad se crea en el almacenamiento {#sec-a49dab56fd86} La validación es *filtrar*, el almacenamiento es *normalizar*. Si el servidor genera un nuevo flujo de bytes, la seguridad y la operación se simplifican. * Formato único → simplifica caché y generación de miniaturas. * Limpieza de metadatos → elimina datos sensibles (GPS EXIF). * Reduce la posibilidad de inyección de carga maliciosa. --- ## Detalles adicionales para una solución robusta {#sec-733fb04d797a} ![Imagen que muestra la defensa contra cargas maliciosas](/media/editor_temp/6/60e76b29-7657-4042-8e5b-3b7b2670a4c7.png) * **Nunca confíes en el nombre de archivo**; genera uno en el servidor (UUID recomendado). * **Considera no recibir la carga directamente en el servidor de aplicación**. En servicios grandes, usa URLs pre‑firmadas para subir directamente a un bucket de almacenamiento y procesa/convierte de forma asíncrona. * **Limita el tiempo de procesamiento y aisla el trabajo**. La conversión de imágenes consume CPU; evita mantener la solicitud web bloqueada y delega a workers/colas. * **Registra y métrica**. Almacena motivos de rechazo (MIME, resolución, tamaño) para detectar patrones de ataque. --- ## Checklist de resumen {#sec-ab5708035e92} 1. **No cargues el archivo completo en memoria**. Usa `read()` solo en la cabeza. 2. **No confíes en extensión/Content‑Type**; verifica MIME con Magic Number. 3. **En la validación, evita `load()`/`verify()`**; comprueba solo la resolución. 4. **No guardes el original**; re‑codifica con transcodificación. 5. **Restablece el puntero con `seek(0)`** en cada paso. La seguridad de la carga comienza con la sospecha del usuario, pero la eficiencia de rendimiento nace de comprender cómo funciona el sistema. Equilibrar ambos aspectos evita caídas y mantiene la experiencia del usuario. --- **Lee también** - [[Facilita la carga de archivos en la web] Dropzone.js completo](/ko/whitedec/2025/12/24/dropzone-js-complete-guide/)