Короткий ответ
Нужно проверять реальный тип содержимого файла по сигнатуре (а не доверять расширению или Content-Type из запроса), ограничивать максимальный размер, генерировать собственное безопасное имя файла на сервере вместо использования присланного имени, и хранить файлы вне корня веб-сервера с ограниченными правами исполнения.
Как это работает подробнее
Расширение и Content-Type в запросе полностью контролируются клиентом и могут быть подделаны, поэтому тип файла стоит дополнительно проверять по сигнатуре содержимого (magic bytes); присланное имя файла может содержать ../ для path traversal, поэтому сервер генерирует собственное уникальное имя (UUID), а оригинальное имя сохраняет отдельно только как метаданные.
Пример кода
example.py
import uuid, imghdr
MAX_SIZE = 5 * 1024 * 1024 # 5 MB
def save_uploaded_avatar(file_bytes: bytes) -> str:
if len(file_bytes) > MAX_SIZE:
raise ValueError("file too large")
kind = imghdr.what(None, h=file_bytes) # проверка по сигнатуре, не по расширению
if kind not in ("jpeg", "png"):
raise ValueError("unsupported file type")
safe_name = f"{uuid.uuid4().hex}.{kind}"
with open(f"/var/app/uploads/{safe_name}", "wb") as f:
f.write(file_bytes)
return safe_name # оригинальное имя пользователя не используется как путьЧто использовать на практике
- Практика. Для изображений дополнительно полезно перекодировать файл через библиотеку обработки изображений, что уничтожает потенциально вредоносный полезный код, встроенный не по спецификации формата; каталог с загруженными файлами не должен давать право на исполнение.
Подводные камни
- Риск. Проверяют только расширение файла (.jpg, .png), пропуская файл с вредоносным содержимым и переименованным расширением; сохраняют файл по оригинальному имени без санитизации, допуская перезапись системных файлов через path traversal; не ограничивают размер загрузки.