Django/Security Загрузка изображений, если не проверять, сервер падает: полный гид по безопасности и эффективности
В веб‑сервисах загрузка изображений часто считается «всегда присутствующей» функцией, которую можно быстро реализовать. Однако эндпоинт для загрузки – это самый прямой путь внешних данных в сервер, и для злоумышленника это точка, где можно нанести максимальный ущерб при минимальных затратах (загрузка веб‑шелла, уязвимости парсера изображений, DoS и т.д.).
В этой статье мы покажем, как одновременно удовлетворить «парадокс» «безопасность – паранойя, ресурсы – экономия».
Ключевые идеи:
- На этапе проверки – «не читаем» (запрещаем декодирование, читаем только заголовки/метаданные)
- На этапе сохранения – «переходим к новому» (транскодируем для очистки)
Суть безопасности загрузки: «не доверяй, пока не убедишься»
Над чем можно доверять при загрузке почти ничего нет.
- Имя файла: меняет пользователь
- Расширение: меняет пользователь
- Content‑Type: указывает клиент
- Содержимое: создаёт злоумышленник
Поэтому стратегия сводится к двум пунктам.
- Быстрый отбор с низкой стоимостью (cheap checks)
- Сохраняем только серверно‑сгенерированный файл (server‑generated artifact)
Распространённое заблуждение: «чтобы проверить, надо прочитать всё»
Многие разработчики делают ошибку, вставляя в проверочный код:
img = Image.open(file)
img.verify() # или img.load()
Проблема в том, что это предоплата ресурсов серверу злоумышленнику.
Почему опасно?
- Раздувающийся архив (Decompression Bomb) – файл выглядит как несколько мегабайт, но после декодирования становится десятками гигабайт.
load()реально декодирует пиксели, быстро истощая память/CPU и приводя к DoS. - Ненужный I/O –
verify()читает файл до конца, а после обычно требуетсяseek(0)/переоткрытие.
Вывод: в этапе проверки не декодируйте пиксели. Достаточно заголовков, метаданных и ограничения разрешения.
Защита – не один шаг, а 3 уровня: Defense in Depth
Загрузка изображений не решается «одним if». Нужно строить слои.
1 уровень: Расширение – ложь, MIME по Magic Number
Имя profile.png ничего не говорит. Нужно читать сигнатуру (Magic Number).
- Не считывайте весь файл. 1–2 КБ достаточно.
- Библиотека:
python-magic(libmagic).
2 уровень: Pillow – «только открытие» и ограничение разрешения
Image.open() обычно не загружает пиксели сразу, а только парсит заголовок. Это позволяет блокировать по разрешению до декодирования.
- Проверяем:
width * height <= MAX_PIXELS. - Не используем
load()/verify().
3 уровень: Лучшее «очищение» – «перерисовать» (Transcoding)
Самый важный принцип.
Не сохраняйте оригинал.
Изображения могут скрывать вредоносные данные в метаданных (EXIF), профилях, «пикселях» и т.д. Если сервер извлечёт только пиксели и сохранит в новом формате, большинство угроз исчезнет.
- Рекомендуется WebP (или AVIF/JPEG) для пересохранения.
- Это обеспечивает очистку + оптимизацию размера + единообразие формата.
Практическая реализация: DRF Serializer (безопасность + экономия памяти)
Ниже код, который воплощает философию «не читаем в проверке, пересоздаём в сохранении».
f.size– метаданные, которые знает Django.- Magic Number – читаем только начало.
- Pillow – только
size. - Финальное сохранение – Transcoding (WebP).
- В каждом шаге
seek(0)возвращает указатель.
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("Ошибка при обработке изображения.")
Как сбалансировать «паранойю» и экономию ресурсов
Ключевой баланс:
Не переусердствуйте в проверке
Проверка – это зона с наибольшим трафиком и возможностью атаки. Любые дорогие операции (например, load()) позволяют злоумышленнику «платить» за ресурсы сервера.
- ✅ Ограничение размера / MIME по заголовку / ограничение разрешения
- ❌ Декодирование пикселей / чтение всего файла / многократное открытие
«Настоящая» безопасность – в сохранении
Проверка – это фильтр, а сохранение – стандартизация. Если сервер генерирует новый поток байтов, безопасность и эксплуатация упрощаются.
- Единый формат → упрощённая стратегия кэширования и генерации превью
- Очистка метаданных → удаление личных данных (EXIF GPS)
- Снижение риска внедрения вредоносных payload
Полный чек‑лист
- Не загружайте файл полностью в память.
read()– только начало, остальное – поток. - Не доверяйте расширению/Content‑Type, проверяйте Magic Number.
- В проверке не используйте
load()/verify(), проверяйте только разрешение. - Не сохраняйте оригинал, пересохраняйте (Transcoding).
- В каждом шаге возвращайте указатель
seek(0).
Безопасность загрузки начинается с подозрения, а эффективность – с понимания, как работает система. Соблюдая оба принципа, вы создадите надёжный порт для изображений.
Смотрите также
Комментариев нет.