# Django/Security Загрузка изображений, если не проверять, сервер падает: полный гид по безопасности и эффективности В веб‑сервисах загрузка изображений часто считается «всегда присутствующей» функцией, которую можно быстро реализовать. Однако эндпоинт для загрузки – это **самый прямой путь внешних данных в сервер**, и для злоумышленника это точка, где можно нанести максимальный ущерб при минимальных затратах (загрузка веб‑шелла, уязвимости парсера изображений, DoS и т.д.). В этой статье мы покажем, как одновременно удовлетворить «парадокс» «безопасность – паранойя, ресурсы – экономия». Ключевые идеи: * **На этапе проверки – «не читаем»** (запрещаем декодирование, читаем только заголовки/метаданные) * **На этапе сохранения – «переходим к новому»** (транскодируем для очистки) --- ## Суть безопасности загрузки: «не доверяй, пока не убедишься» {#sec-98341e6a33e1} Над чем можно доверять при загрузке почти ничего нет. * Имя файла: меняет пользователь * Расширение: меняет пользователь * Content‑Type: указывает клиент * Содержимое: создаёт злоумышленник Поэтому стратегия сводится к двум пунктам. 1. **Быстрый отбор с низкой стоимостью** (cheap checks) 2. **Сохраняем только серверно‑сгенерированный файл** (server‑generated artifact) --- ## Распространённое заблуждение: «чтобы проверить, надо прочитать всё» {#sec-f9b6b6b99eb2} Многие разработчики делают ошибку, вставляя в проверочный код: ```python img = Image.open(file) img.verify() # или img.load() ``` Проблема в том, что это **предоплата ресурсов серверу злоумышленнику**. ### Почему опасно? {#sec-7ed56ee48a1d} * **Раздувающийся архив (Decompression Bomb)** – файл выглядит как несколько мегабайт, но после декодирования становится десятками гигабайт. `load()` реально декодирует пиксели, быстро истощая память/CPU и приводя к DoS. * **Ненужный I/O** – `verify()` читает файл до конца, а после обычно требуется `seek(0)`/переоткрытие. **Вывод:** в этапе проверки не декодируйте пиксели. Достаточно заголовков, метаданных и ограничения разрешения. --- ## Защита – не один шаг, а 3 уровня: Defense in Depth {#sec-5756153c302b} Загрузка изображений не решается «одним if». Нужно строить слои. ### 1 уровень: Расширение – ложь, MIME по Magic Number {#sec-bbad131e1332} Имя `profile.png` ничего не говорит. Нужно читать **сигнатуру (Magic Number)**. * Не считывайте весь файл. **1–2 КБ** достаточно. * Библиотека: `python-magic` (libmagic). ### 2 уровень: Pillow – «только открытие» и ограничение разрешения {#sec-f0a8fb4bda8a} `Image.open()` обычно **не загружает пиксели сразу**, а только парсит заголовок. Это позволяет блокировать по **разрешению** до декодирования. * Проверяем: `width * height <= MAX_PIXELS`. * Не используем `load()`/`verify()`. ### 3 уровень: Лучшее «очищение» – «перерисовать» (Transcoding) {#sec-398d6ae888a9} Самый важный принцип. > Не сохраняйте оригинал. Изображения могут скрывать вредоносные данные в метаданных (EXIF), профилях, «пикселях» и т.д. Если сервер извлечёт только пиксели и сохранит в новом формате, большинство угроз исчезнет. * Рекомендуется **WebP** (или AVIF/JPEG) для пересохранения. * Это обеспечивает **очистку + оптимизацию размера + единообразие формата**. --- ## Практическая реализация: DRF Serializer (безопасность + экономия памяти) {#sec-7b99e0ec685d} Ниже код, который воплощает философию «не читаем в проверке, пересоздаём в сохранении». * `f.size` – метаданные, которые знает Django. * Magic Number – читаем только начало. * Pillow – только `size`. * Финальное сохранение – Transcoding (WebP). * В каждом шаге `seek(0)` возвращает указатель. ```python from io import BytesIO import magic from PIL import Image, ImageOps, UnidentifiedImageError from rest_framework import serializers # Политика (настраивайте под сервис) MAX_SIZE = 5 * 1024 * 1024 # 5 МБ MAX_PIXELS = 4_194_304 # 2048 × 2048 ≈ 4 МП ALLOWED_MIME = {"image/png", "image/jpeg", "image/webp"} class SecureImageUploadSerializer(serializers.Serializer): file = serializers.ImageField() def validate_file(self, f): # [1] Ограничение размера – самый быстрый фильтр if f.size > MAX_SIZE: raise serializers.ValidationError("Файл слишком большой.") # [2] Проверка MIME по Magic Number 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("Неподдерживаемый формат файла.") # [3] Ограничение разрешения без декодирования try: with Image.open(f) as img: w, h = img.size if (w * h) > MAX_PIXELS: raise serializers.ValidationError("Разрешение изображения слишком велико.") except UnidentifiedImageError: raise serializers.ValidationError("Недопустимое изображение.") except Exception: raise serializers.ValidationError("Ошибка при проверке изображения.") finally: f.seek(0) return f def create(self, validated_data): f = validated_data["file"] # Финальное сохранение – сервер генерирует новый файл (очистка) try: with Image.open(f) as img: img = ImageOps.exif_transpose(img) 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() # Здесь сохраняйте safe_bytes в хранилище # - генерируйте имя UUID # - используйте шардирование каталогов # - в БД храните только ключ, а не оригинальное имя return safe_bytes except Exception: raise serializers.ValidationError("Ошибка при обработке изображения.") ``` --- ## Как сбалансировать «паранойю» и экономию ресурсов {#sec-7ec5087de2e9} Ключевой баланс: ### Не переусердствуйте в проверке {#sec-5c32bd66af4d} Проверка – это зона с наибольшим трафиком и возможностью атаки. Любые дорогие операции (например, `load()`) позволяют злоумышленнику «платить» за ресурсы сервера. * ✅ Ограничение размера / MIME по заголовку / ограничение разрешения * ❌ Декодирование пикселей / чтение всего файла / многократное открытие ### «Настоящая» безопасность – в сохранении {#sec-a49dab56fd86} Проверка – это фильтр, а сохранение – стандартизация. Если сервер генерирует новый поток байтов, безопасность и эксплуатация упрощаются. * Единый формат → упрощённая стратегия кэширования и генерации превью * Очистка метаданных → удаление личных данных (EXIF GPS) * Снижение риска внедрения вредоносных payload --- ## Полный чек‑лист {#sec-ab5708035e92} 1. **Не загружайте файл полностью в память.** `read()` – только начало, остальное – поток. 2. **Не доверяйте расширению/Content‑Type, проверяйте Magic Number.** 3. **В проверке не используйте `load()`/`verify()`, проверяйте только разрешение.** 4. **Не сохраняйте оригинал, пересохраняйте (Transcoding).** 5. **В каждом шаге возвращайте указатель `seek(0)`.** Безопасность загрузки начинается с подозрения, а эффективность – с понимания, как работает система. Соблюдая оба принципа, вы создадите надёжный порт для изображений. --- **Смотрите также** - [[Упрощённая загрузка файлов в вебе] Полное руководство по Dropzone.js](/ko/whitedec/2025/12/24/dropzone-js-complete-guide/)