Entendiendo los métodos open(), verify() y load() de Pillow desde la perspectiva de la seguridad

La carga de imágenes no es simplemente “recibir un archivo de imagen”; es pasar una entrada externa a un decodificador (parser). Por eso, los tres métodos de Pillow son más importantes por cuándo se invocan (es decir, cuándo se abre la superficie de ataque) que por su descripción funcional.


open() no es una función para “cargar píxeles”

Image.open() funciona de forma perezosa. Solo abre el archivo y lo identifica; los datos de píxeles pueden no leerse todavía. Además, el descriptor de archivo puede permanecer abierto.

En seguridad y operación, el uso correcto de open() es sencillo:

  • Con open() se obtiene información ligera como el formato y el ancho/alto
  • Se bloquea según la política: formatos permitidos, resolución máxima/número de píxeles, límite de tamaño de carga
  • Se procede a la siguiente fase (verificación/decodificación)

En otras palabras, open() debe usarse como una herramienta para extraer información que permita decidir antes de la decodificación.


¿Qué garantiza verify() y qué no?

verify() intenta comprobar si el archivo está dañado, pero no decodifica los datos de imagen. Si encuentra un problema, lanza una excepción y, para seguir usando la imagen, se debe volver a abrir.

Desde la perspectiva de la seguridad, las conclusiones son dos:

  • Ventaja: evita la decodificación (una operación pesada) y filtra rápidamente archivos dañados.
  • Limitación: pasar verify() no significa “seguro”; simplemente indica que el archivo no parece estar gravemente dañado. Los problemas pueden aparecer recién en load().

load() es peligroso si se llama indiscriminadamente en la fase de validación

load() realmente decodifica (incluida la descompresión) y carga los píxeles en memoria. Este punto es una superficie de ataque directa para DoS (agotamiento de recursos). Un archivo que parece pequeño puede generar una gran cantidad de datos al decodificar.

Pillow advierte sobre el riesgo de “bomba de descompresión” y lanza una excepción cuando se supera un umbral predeterminado (por ejemplo, 128 Mpx). Django, por la misma razón, utiliza verify() en lugar de load() durante la validación de carga de imágenes. El código fuente incluye comentarios como “load() carga la imagen completa en memoria y es un vector DoS” y realmente llama a Image.open() seguido de verify().


En Django/DRF: llamar a verify() en ImageField puede ser redundante

La validación de ImageField en formularios de Django ya ejecuta internamente Image.open() + verify(). El serializers.ImageField de DRF delega la validación a Django con un comentario que indica que se llama a la validación de Django.

Por lo tanto, si ya usas serializers.ImageField en DRF:

  • Llamar a verify() en validate() solo para comprobar la integridad suele ser una tarea redundante.
  • Si necesitas una validación de negocio o seguridad adicional, considera usar FileField en lugar de ImageField y diseñar tu propio pipeline de validación, lo que hace más claro el coste y la responsabilidad.

Cómo garantizar la “seguridad” de los archivos subidos por el usuario

Diagrama de procesamiento seguro de archivos subidos

La respuesta más realista es la siguiente:

“No utilices el archivo original tal cual; decodifica en el servidor y guarda el resultado como un nuevo archivo.”

  • Con open() obtén información ligera (ancho/alto/formato) y bloquea según la política.
  • Con verify() elimina archivos claramente dañados.
  • Para los que pasan, decodifica en un entorno controlado y normaliza a píxeles estándar como RGB/RGBA.
  • Re-codifica el resultado en el formato elegido por el servidor y crea un nuevo archivo.
  • El servicio solo almacena y sirve el archivo re-codificado.

Esta estrategia permite que el servidor controle la forma final de la salida, eliminando metadatos innecesarios o estructuras extrañas del original.

Sin embargo, la re-codificación implica una decodificación equivalente a load(). Por eso, primero establece límites de píxeles/memoria (para defenderse de la bomba de descompresión) y, si es posible, ejecuta el proceso en un trabajador o proceso aislado.


Resumen

  • open(): “identificación + extracción de información ligera” (los píxeles pueden no estar cargados).
  • verify(): “filtro inicial de integridad” (sin decodificación; se necesita volver a abrir para usar).
  • load(): “punto donde comienza la decodificación y el uso de memoria” (evitar su uso indiscriminado en la fase de validación).
  • Respuesta práctica para la seguridad de la carga: confía únicamente en el archivo re-codificado por el servidor (con límites y aislamiento adecuados).

Enlaces relacionados :