Безопасная работа с файлами пользователей: что выбрать
Загрузка файла на сервер — это не просто сохранение multipart-запроса. Сравниваем локальное хранилище, объектное хранилище и карантин со сканированием, а затем выбираем схему, которая не превращает загрузку в дыру.

Сохранить файл пользователя в каталог uploads можно за несколько минут, но безопасная схема начинается не с выбора папки. Нужно решить, кто принимает файл, где он лежит до проверки, когда становится доступен другим пользователям и что произойдёт при повторной загрузке, вредоносном содержимом или переполнении диска.
Сначала разделите загрузку, проверку и выдачу
Типичная ошибка — принять файл, взять его исходное имя, положить в каталог сайта и сразу отдать по URL. В этой цепочке сразу несколько проблем: имя может содержать управляющие символы или попытку обхода каталогов, расширение не доказывает тип содержимого, а загруженный HTML или SVG способен выполнить код в браузере.
Безопаснее считать файл недоверенным объектом до конца проверки. У него должны быть отдельные состояния:
- принят — запрос прошёл базовые ограничения по размеру и формату;
- на проверке — файл ещё не должен быть доступен обычному пользователю;
- разрешён — прошёл проверки и может выдаваться через контролируемый endpoint или CDN;
- отклонён — сохранён для расследования или удалён по политике хранения.
Карантин — отдельное хранилище для объектов, которые уже приняты системой, но ещё не допущены к скачиванию. Это не антивирус сам по себе, а архитектурное состояние между загрузкой и публикацией.
Главное правило: файл не становится безопасным только потому, что его назвали
.jpgи положили вне корня сайта.
Четыре рабочих подхода
1. Локальный каталог вне web root
Приложение принимает multipart-запрос, генерирует случайный идентификатор, сохраняет объект вне каталога, из которого веб-сервер раздаёт файлы, а скачивание выполняет отдельный обработчик после проверки прав.
Подход прост для одного сервера и небольшого внутреннего сервиса. Он не требует отдельного хранилища, но привязывает данные к диску конкретного узла. При горизонтальном масштабировании файл, загруженный на один сервер, может не оказаться на другом.
Имя пользователя храните только как метаданные. Физический путь строится из серверного идентификатора, например UUID, а не из строки, пришедшей в запросе. Даже если клиент прислал invoice.pdf, на диске лучше иметь непрозрачное имя без возможности исполнения.
2. Объектное хранилище с закрытым бакетом
Файлы лежат в S3-совместимом объектном хранилище, а приложение хранит в базе только идентификатор объекта, владельца, размер, заявленный и определённый тип, статус проверки и дату удаления.
Это удобнее для нескольких экземпляров приложения и больших объёмов. Диск веб-сервера не становится узким местом, а срок жизни объектов и резервное копирование настраиваются отдельно. Цена — зависимость от сети, стоимость операций и необходимость правильно закрыть публичный доступ.
Если браузер загружает файл напрямую в хранилище по временной ссылке, сервер всё равно должен выдать короткоживущую ссылку только после проверки пользователя и ограничить ключ объекта, размер и срок действия. Временная ссылка на публичный бакет не заменяет авторизацию.
3. Карантин и асинхронная проверка
После загрузки объект попадает в карантин, затем фоновый обработчик проверяет размер, сигнатуру формата, архивы, метаданные и вредоносное содержимое. Только после успешной проверки объект переводится в разрешённое хранилище или получает статус ready.
Это лучший вариант для документов, аватаров из непроверенных источников и файлов, которые будут скачивать другие пользователи. Пользователь должен видеть статус «проверяется», а не получать прямую ссылку на исходный объект.
Цена — сложность очереди и повторной обработки. Нужно хранить причину отказа, ограничивать число попыток и продумать сценарий, когда сканер недоступен. Ошибка проверки не должна автоматически означать «разрешить».
4. Специализированный сервис обработки
Для изображений, видео и PDF можно вынести преобразование в отдельный рабочий процесс: исходник остаётся закрытым, а пользователю выдаётся производная копия — например, уменьшенное изображение или очищенный PDF.
Такой подход уменьшает риск публикации исходного содержимого и позволяет ограничить опасные форматы. Но конвертеры сами обрабатывают сложные входные данные, поэтому их запускают с минимальными правами, лимитами CPU и памяти и без доступа к внутренней сети.
| Подход | Когда выбирать | Плюсы | Цена и риск |
|---|---|---|---|
| Локальный каталог вне web root | Один сервер, небольшой объём | Просто внедрить, низкая стоимость | Сложно масштабировать и восстанавливать |
| Закрытое объектное хранилище | Несколько экземпляров, большие файлы | Масштабирование, отдельный жизненный цикл | Нужны настройки доступа и бюджет на операции |
| Карантин со сканированием | Документы, публичные загрузки | Файл не публикуется до проверки | Очередь, задержка и обслуживание сканера |
| Конвертация в отдельном сервисе | Изображения, видео, PDF | Можно выдавать безопасную производную копию | Новая изолированная подсистема и ресурсы |
Что проверять независимо от хранилища
Хранилище не исправляет ошибки на уровне HTTP-обработчика. Минимальный набор проверок должен выполняться до публикации объекта.
- Размер. Ограничьте размер на уровне прокси, приложения и хранилища. Лимит только в интерфейсе браузера бесполезен: запрос можно отправить напрямую.
- Имя. Не используйте исходное имя как путь и не разрешайте пользователю задавать ключ объекта. Сохраните исходное имя отдельно для отображения, экранируя его при выводе в HTML.
- Формат. Сверяйте расширение, MIME-тип и сигнатуру файла. Заголовок
Content-Typeприходит от клиента и не считается доказательством. - Архивы. Ограничьте количество вложенных файлов, суммарный распакованный размер и глубину вложенности. Иначе небольшой архив может занять весь диск или память.
- Права. Проверяйте владельца и разрешение на каждый запрос скачивания. Наличие идентификатора объекта в URL не доказывает право доступа.
- Выдача. Для пользовательских файлов задавайте безопасный тип ответа и продумывайте скачивание вместо отображения в браузере. Особенно осторожно обращайтесь с HTML, SVG и файлами, которые могут содержать активное содержимое.
- Логи. Записывайте пользователя, размер, результат проверки, причину отказа и идентификатор объекта, но не содержимое файла и не секретную временную ссылку.
Когда какой вариант выбирать
Для внутренней панели на одном сервере обычно достаточно каталога вне web root и выдачи через авторизованный обработчик. Если приложение работает на нескольких узлах или принимает большие файлы, выбирайте закрытое объектное хранилище. Для публичных загрузок добавляйте карантин: прямой путь «загрузили — сразу показали» здесь слишком рискован.
Если пользователю нужен только результат обработки, не выдавайте исходник вообще. Сохраните его отдельно, проведите проверку, создайте производную копию и публикуйте только её. Это снижает последствия ошибки, но не отменяет изоляцию конвертера.
Что ломается на практике
«Фильтруем только расширение»
Файл с разрешённым расширением может иметь другой формат или содержать активное содержимое. Проверяйте сигнатуру и фактическое содержимое, а список разрешённых форматов делайте коротким. Если форматы неизвестны заранее, безопаснее запретить загрузку, чем пытаться угадать намерение пользователя.
«Переименуем, но оставим каталог публичным»
Случайное имя уменьшает риск угадывания, но не решает проблему доступа. Ссылка может попасть в историю браузера, логи, письма и сторонние системы. Публичность должна быть осознанной, а не следствием настройки веб-сервера по умолчанию.
«Сканер недоступен — пропустим файл»
Это опасный отказоустойчивый режим. При ошибке сканера объект должен остаться в карантине, а система — показать понятный статус и поставить повторную попытку с ограничением числа повторов.
«Трансформер запустим с правами приложения»
Если обработчик изображения или PDF будет скомпрометирован, злоумышленник получит права процесса. Запускайте обработку в отдельном контейнере или сервисе, ограничивайте файловую систему, сеть, память и время выполнения.
Что измерять после внедрения
Начните с журнала переходов состояний: сколько объектов принято, сколько зависло в карантине, сколько отклонено и сколько времени занимает проверка. Отдельно отслеживайте долю ошибок загрузки, средний и максимальный размер файла, заполнение диска или объём объектного хранилища.
Следующий шаг — провести негативные тесты на копиях среды: превышение размера, неверный MIME-тип, двойное расширение, повреждённый архив, попытка скачать чужой объект и недоступный сканер. После этого проверьте настройки прокси и хранилища по документации конкретной версии используемого SDK и сервиса: названия параметров и значения лимитов между реализациями различаются.
Проектная работа: От проблемы до защиты: продукт, этапы, вопросы комиссии
Спросить за 15 ₽Источники: OWASP File Upload Cheat Sheet, Amazon S3 User Guide: presigned URLs
Частые вопросы
Можно ли хранить пользовательские файлы в локальной папке приложения?
Достаточно ли проверить расширение и Content-Type?
Нужно ли сканировать аватары и небольшие изображения?
Что делать, если сканер временно не работает?
Можно ли давать пользователю прямую ссылку на объектное хранилище?
Что делать, если у меня другая версия SDK или S3-совместимого хранилища?
Какие подводные камни появляются в продакшене?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.