Загрузка изображения — это не просто «получение файла», а передача внешнего ввода в декодер (парсер). Поэтому важнее не то, что делают методы Pillow, а когда они вызываются (то есть когда открывается поверхность атаки).


open() — это не «поднятие пикселей»

Image.open() работает лениво. Он открывает файл и определяет его формат, но пиксельные данные могут не считываться. Файл может оставаться открытым.

В безопасной и операционной практике open() используется так:

  • Сначала с помощью open() получаем лёгкую информацию: формат, ширину/высоту
  • На основе политики блокируем: разрешённые форматы, максимальное разрешение/число пикселей, лимит размера загрузки
  • Затем переходим к следующему этапу (проверка/декодирование)

То есть open() безопасно использовать как инструмент для извлечения информации до декодирования.


Что гарантирует verify() и что не гарантирует

verify() пытается убедиться, что файл не повреждён, но не декодирует данные изображения. Если обнаружены проблемы, генерируется исключение, и чтобы продолжить работу с изображением, его нужно открыть заново.

Выводы с точки зрения безопасности:

  • Плюс: быстро отфильтровывает повреждённые файлы, не выполняя тяжёлого декодирования
  • Минус: прохождение verify() не означает полной безопасности; проблемы могут проявиться только при load()

load() опасен, если вызывать его в процессе проверки

load() действительно декодирует (включая распаковку) и загружает пиксели в память. Это сразу создаёт поверхность DoS-атаки, так как даже маленький файл может распаковаться в огромный объём данных.

Pillow предупреждает о «декомпрессионной бомбе» и имеет защиту по умолчанию (например, порог 128 Мп). Django по той же причине при валидации изображений использует verify() вместо load(). В коде есть комментарий: «load() загружает всё изображение в память и может стать вектором DoS».


При использовании Django/DRF: ImageField уже вызывает verify() — дублирование может быть избыточным

Валидация ImageField в Django использует Image.open() + verify(). Аналогично serializers.ImageField в DRF делегирует проверку Django.

Если вы уже используете serializers.ImageField, то:

  • Вызов verify() в validate() обычно избыточен
  • Для более строгой бизнес-валидации можно принимать файл через FileField и самостоятельно строить пайплайн проверки, что делает ответственность более прозрачной

Как обеспечить «безопасность» загруженных файлов

Диаграмма безопасной обработки загруженных файлов

Практичное решение:

  1. Не используйте оригинальный файл напрямую: декодируйте его на сервере и сохраняйте только результат.
  2. С помощью open() считываем лёгкую информацию (формат, размеры) и применяем первичную фильтрацию.
  3. verify() удаляет явно повреждённые файлы.
  4. В ограниченной среде декодируем и нормализуем в стандартные пиксели (RGB/RGBA).
  5. Перекодируем в выбранный сервером формат и сохраняем новый файл.
  6. Сервис хранит и обслуживает только серверно перекодированный файл.

Преимущество: сервер контролирует конечный формат, удаляя лишние метаданные и аномалии. Однако перекодирование всё равно включает load(), поэтому важно установить ограничения по количеству пикселей/памяти и выполнять это в изолированном процессе.


Итоги

  • open() — «идентификация + сбор лёгкой информации» (пиксели могут отсутствовать)
  • verify() — «первичный фильтр повреждений» (без декодирования, при дальнейшей работе требуется повторное открытие)
  • load() — «начало декодирования/использования памяти» (не использовать в процессе проверки)
  • Практический подход к безопасной загрузке: доверять только серверно перекодированным файлам (с ограничениями и изоляцией)

Смотрите также: