checklistsecuritymobile

Чек-лист тестирования загрузки файлов: типы, размеры, безопасность и UX

Поле загрузки файла выглядит безобидно — кнопка «Выбрать файл» и всё. Но это единственное место, где пользователь кладёт произвольные байты на твой сервер. Здесь сходятся сразу три класса проблем: функциональные (не тот тип, битый файл), безопасностные (загрузили вебшелл) и UX (загрузка на 40 МБ без прогресса и отмены). Поэтому аплоад — стабильный источник и багов, и настоящих инцидентов, и почти всегда недотестирован.

Ниже — чек-лист по разделам: от типов и размеров до хранения. Не «прокликать happy path», а именно попытаться сломать.

Типы и MIME — не верь расширению

Главный принцип: расширение и заголовок Content-Type контролирует клиент, значит им нельзя доверять.

  • Расширение vs реальное содержимое. Переименуй shell.php в photo.jpg — примет ли сервер? А наоборот, валидный PNG с расширением .txt? Настоящий тип определяется по magic bytes (сигнатуре), а не по имени.
  • Двойное расширение. invoice.pdf.exe, avatar.jpg.svg — какое расширение возьмёт валидация, первое или последнее? А как файл отдастся обратно?
  • Подмена Content-Type. Через прокси (Burp) поставь Content-Type: image/png на исполняемый файл. Валидация на MIME из запроса — дырка.
  • Пустой и странный MIME. application/octet-stream, отсутствующий Content-Type, экзотические типы.
  • Полиглот-файлы. Файл, валидный сразу как GIF и как JS/PHP (GIFAR и родственники). Проходит проверку «это картинка» — и исполняется как скрипт.
  • Регистр и пробелы. .JPG, .Jpg, photo.jpg (пробел в конце), unicode-омоглифы в расширении.
  • Whitelist, а не blacklist. Проверь, что разрешён явный список допустимых типов, а не «запретим .exe и .php». Блэклист всегда неполный.

Размеры и количество

  • 0 байт. Пустой файл — падение, 500 или корректная ошибка?
  • 1 байт / обрезанный. Валидный заголовок, но файл оборван — как ведёт себя обработчик/превью.
  • Гигантский файл. Больше лимита, ровно на границе лимита, на 1 байт больше. Лимит проверяется на клиенте и на сервере (клиентский легко обойти через API).
  • Zip-bomb / декомпрессия. Если принимаешь архивы или что-то распаковываешь (docx, картинки с огромным разрешением) — маленький файл, который разворачивается в гигабайты. «Пиксель-бомба»: 64000×64000 PNG весит килобайты, а в память при ресайзе просит терабайты.
  • Множественная загрузка. Много файлов разом, лимит на количество, суммарный размер батча, один битый в середине пачки.
  • Долгая/медленная загрузка. Slowloris-подобная — очень медленная отправка большого тела: держит ли сервер соединение вечно, есть ли таймаут.

Имена файлов

Имя файла — это тоже пользовательский ввод, причём его часто кладут в путь на диске или в заголовок ответа.

  • Path traversal. ../../etc/passwd, ..\..\web.config — можно ли выйти из папки загрузок и перезаписать чужой файл. Классика RCE.
  • Спецсимволы и управляющие. ;, |, &, $(), перевод строки, NULL-байт %00 (обрезание имени), кавычки.
  • Очень длинное имя. 255+ символов, 4096 символов — лимиты ФС и БД.
  • Unicode и эмодзи. Кириллица, арабица (RTL), эмодзи, RTL-override U+202E (маскирует exe как gpj).
  • Зарезервированные имена. На Windows: CON, PRN, NUL, COM1, AUX.
  • Дубли. Два файла с одинаковым именем — перезапись или уникализация? Гонка при одновременной загрузке одинакового имени.
  • Заголовок ответа. Если имя возвращается в Content-Disposition — проверь инъекцию заголовка и корректный экранинг.

Безопасность — здесь ставки выше всего

  • Куда сохраняется и как отдаётся. Если файлы лежат в веб-доступной папке и отдаются по прямой ссылке — залитый .php/.jsp может исполниться. Правильно: хранить вне webroot или в объектном сторадже, отдавать через контроллер с правильным Content-Type и Content-Disposition: attachment.
  • SVG = исполняемый. SVG — это XML, в него встраивается <script> и обработчики событий. Открытый инлайном в браузере жертвы — это stored XSS. Либо запрещай SVG, либо жёстко санитизируй (например, через DOMPurify), либо отдавай только как attachment.
  • EXIF и приватность. Фото с телефона несёт GPS-координаты, модель, время. Публикуешь аватар — не публикуй геолокацию автора. Проверь, что метаданные стрипаются.
  • Антивирус / скан. Загружаемое сканируется (ClamAV/облачный скан)? Тест — файл EICAR (безопасная антивирусная тест-сигнатура): ловится ли.
  • Авторизация. Может ли пользователь A прочитать/перезаписать файл пользователя B, подставив чужой ID или угадав имя? Прямая ссылка на файл требует проверки прав, а не «секретности» URL.
  • IDOR на скачивание. Перебор ?file=1001, 1002 — отдаются ли чужие документы.
  • XXE через офисные форматы. docx/xlsx — это zip с XML; парсер с включёнными внешними сущностями = XXE.
  • Стоимость и абьюз. Аплоад без rate limit = вектор для заполнения диска и денег на трафик/обработку.

Обработка, превью и рендер

  • Битый/обрезанный файл при генерации превью — graceful error, а не 500 и не падение воркера.
  • Ориентация по EXIF. Фото «лежит на боку» — учитывает ли ресайзер Orientation.
  • Ресайз и качество. Прозрачность PNG, анимация GIF/WebP, цветовые профили, слишком большое разрешение (см. пиксель-бомбу выше).
  • Асинхронная обработка. Если конвертация в фоне — что показывается пользователю, пока не готово; что если воркер упал; повторная попытка.
  • Идемпотентность. Повторная загрузка того же файла (дабл-тап, ретрай сети) не должна плодить дубли или списывать что-то дважды.

UX и надёжность

  • Прогресс. Есть ли индикатор для больших файлов; корректен ли процент.
  • Отмена и докачка. Можно ли отменить; продолжается ли загрузка после обрыва сети (resumable).
  • Обрыв сети посередине. На мобильном — Wi-Fi→cellular, лифт, метро. Частичный файл на сервере не должен считаться валидным.
  • Drag & drop и вставка из буфера (скриншот Ctrl+V), выбор из камеры/галереи на мобильном.
  • Понятные ошибки. «Слишком большой файл (макс 10 МБ)» вместо «Ошибка 413». Ошибка про запрещённый тип называет разрешённые.
  • Оффлайн и повторы. Что происходит без сети; не отправляется ли форма дважды.
  • Доступность. Кнопка достижима с клавиатуры и озвучивается скринридером, состояние загрузки анонсируется.

Хранение и жизненный цикл

  • Где лежит: объектный сторадж (S3 и аналоги) vs локальный диск; приватный бакет vs публичный.
  • Осиротевшие файлы. Загрузил, но форму не отправил — файл остаётся навсегда? Есть ли уборка временных.
  • Удаление. При удалении сущности удаляется ли сам файл; удаляются ли кэш/CDN-копии и превью.
  • Лимиты и квоты на пользователя; поведение при исчерпании.

Сводный чек-лист

  • Тип определяется по magic bytes, не по расширению и не по Content-Type из запроса; whitelist разрешённых типов
  • Двойные расширения, полиглоты, подмена MIME через прокси — отбиты
  • Границы размера: 0 байт, обрезанный, ровно лимит, лимит+1; проверка на сервере, не только на клиенте
  • Zip-/пиксель-бомба не роняет обработку
  • Имя файла санитизируется: path traversal, спецсимволы, NULL, RTL-override, длина, дубли
  • Файлы вне webroot или в сторадже; отдаются контроллером с attachment, не исполняются
  • SVG запрещён/санитизирован; EXIF-метаданные стрипаются; вирус-скан (EICAR) работает
  • Авторизация на чтение/удаление; нет IDOR при скачивании
  • Прогресс, отмена, обрыв сети, drag&drop, понятные ошибки, доступность
  • Осиротевшие файлы убираются; удаление сущности чистит файл, превью и CDN

Инструменты

  • Burp Suite / OWASP ZAP — подмена Content-Type, имени, повтор запроса в обход клиентской валидации.
  • file (по magic bytes) и exiftool — проверить, как система определяет тип и что с метаданными.
  • EICAR тест-файл — проверить, что антивирус-скан вообще работает.
  • Готовые «злые» картинки/полиглоты и генераторы декомпрессионных бомб — для проверки обработки.

Аплоад стоит воспринимать не как «прикрепить картинку», а как приём недоверенных данных на свою инфраструктуру. Тестируй с этой позиции — и половина находок появится сама.