Файлы пользователей без сюрпризов: безопасные приёмы
Загрузка файла — это не просто поле в форме: пользователь может прислать исполняемый файл, подменить тип или заполнить временное хранилище. Разбираем практичный контур приёма, проверки и выдачи файлов для веб-приложения.

Пользователь загрузил картинку, а сервер сохранил её в доступную из интернета папку — и вместе с ней, возможно, получил исполняемый файл, вредоносный документ или архив, который развернёт диск. Надёжнее разделить приём, проверку и публикацию: пока файл не прошёл контроль, приложение не должно отдавать его другим пользователям. Ниже — рабочие приёмы для веб-приложения за Nginx 1.26.x; примеры кода не привязаны к конкретному фреймворку.
Сначала определите, что именно вы разрешаете
Фраза «разрешаем загрузку файлов» слишком широкая для настройки защиты. Составьте короткую политику для каждого сценария: аватар, документ к обращению и архив резервной копии — разные типы данных с разными рисками. Например, аватару обычно не нужны SVG и архивы, а документам может потребоваться скачивание, но не просмотр в браузере.
Для каждого сценария зафиксируйте список допустимых расширений, максимальный размер, максимальное число файлов за запрос и срок хранения. Лимиты должны совпадать в приложении и на входном прокси: иначе запрос раньше остановит один слой, а временный диск успеет заполниться на другом. Конкретные значения выбирайте по назначению сервиса и доступной квоте, а не копируйте универсальное число.
Термин: MIME-тип — метка формата файла, например image/jpeg. Браузер может передать её вместе с запросом, но это значение сообщает клиент и само по себе не доказывает, что содержимое действительно является изображением.
Три схемы хранения и их цена
Главный выбор — когда и где файл становится доступен. Публиковать его сразу после загрузки проще, но тогда между приёмом и проверкой возникает окно риска. Для личных кабинетов и пользовательских документов обычно безопаснее отдельная карантинная зона, а для большой нагрузки — прямой приём в закрытое объектное хранилище с последующей проверкой.
| Подход | Плюс | Цена и риск | Когда выбирать |
|---|---|---|---|
| Сохранить в веб-каталог сразу | Минимум компонентов, файл быстро доступен | Ошибка в настройке может сделать файл исполняемым или публичным до проверки | Только для заранее обработанного безопасного содержимого; не как вариант по умолчанию |
| Карантин, проверка, затем публикация | Можно проверять тип, содержимое и права до выдачи | Нужны очередь обработки, временное место и обработка статуса ожидания | Большинство приложений с документами и изображениями |
| Прямая загрузка в закрытое объектное хранилище | Меньше трафика и нагрузки на приложение при больших файлах | Сложнее контролировать права, срок действия ссылки и лимиты; проверка всё равно нужна | Большие файлы и высокая параллельность, если команда умеет сопровождать хранилище |
Во втором и третьем вариантах интерфейс должен показывать файл как ожидающий проверки, а не как готовый. Статус меняется на доступный только после успешных проверок. Ошибка сканера, тайм-аут или потерянное событие очереди не должны трактоваться как разрешение на публикацию.
Проверяйте не имя, а содержимое и сценарий
Имя файла — пользовательский ввод. Не используйте его как путь на диске и не склеивайте с каталогом хранения: в имени могут быть разделители, управляющие символы и неожиданные расширения. Создавайте внутренний случайный идентификатор, а исходное имя храните отдельно только если оно нужно интерфейсу. При выдаче экранируйте его и формируйте заголовок скачивания безопасным способом.
Проверка должна состоять из нескольких независимых шагов:
- Проверить запрос до сохранения. Требуйте ожидаемый способ загрузки, проверьте авторизацию, лимит размера и число частей запроса. Убедитесь, что лимит задан не только в приложении, но и на прокси. Результат: слишком большой запрос отклоняется до того, как временное хранилище исчерпает квоту.
- Сверить расширение с разрешённым списком. Нормализуйте имя и не считайте двойное расширение доказательством типа: файл report.pdf.exe не становится документом из-за части .pdf. Результат: приложение принимает только сценарии, которые действительно поддерживает.
- Определить формат по содержимому. Сравните заявленный тип с сигнатурой и разберите файл подходящим парсером. Ни расширение, ни MIME-метка из браузера не заменяют эту проверку. Результат: файл с несовпадающим форматом уходит в отказ или ручную проверку.
- Проверить опасные форматы и размер после распаковки. Для архивов задайте ограничения на число файлов и общий объём распаковки; для документов и изображений учитывайте уязвимости парсеров и чрезмерные ресурсы обработки. Результат: тестовый архив с чрезмерным распаковочным объёмом не проходит в рабочую очередь.
- Сохранить в карантин и проверить. Запустите антивирусную проверку, если она нужна вашему сценарию, и применяйте безопасную обработку формата там, где это возможно. Сканирование снижает риск, но не гарантирует безопасность. Результат: к выдаче допускаются только файлы с завершённой проверкой и разрешённым статусом.
Для изображений, которые сервису нужно показывать, отдельная обработка с декодированием и повторным сохранением часто безопаснее, чем публикация исходника. Она может удалить лишние метаданные, но не подходит для файлов, где важно сохранить оригинал. Для SVG, HTML и других активных форматов сначала решите, действительно ли пользователям нужен просмотр в браузере. Если нет — запретите их либо выдавайте только как скачивание.
Хранение и выдача: не превращайте ссылку в разрешение
Держите пользовательские файлы вне веб-корня либо в закрытом бакете. Прямой URL не должен открывать объект только потому, что его идентификатор сложно угадать: перед каждой выдачей приложение проверяет, имеет ли текущий пользователь право на этот файл. Для временных ссылок задавайте короткий срок действия и ограниченный доступ к конкретному объекту. Не записывайте такие ссылки в публичные логи и не отправляйте их в открытые каналы.
Для загрузки в браузер задавайте корректный тип содержимого и выбирайте скачивание вместо встроенного отображения там, где формат активен или не нужен для просмотра. Заголовок X-Content-Type-Options со значением nosniff помогает не полагаться на угадывание браузером типа содержимого, но не заменяет проверку файла и контроль доступа. Не отдавайте исходное пользовательское имя в пути хранения.
Файл считается безопасным не тогда, когда загрузка завершилась, а когда приложение проверило его и решило, кому и как его можно выдать.
Что чаще всего ломается
- Проверили только расширение. Переименованный файл проходит контроль. Добавьте проверку содержимого и разбор поддерживаемым форматом.
- Сканер недоступен. Если ошибка или тайм-аут превращают статус в «готов», карантин становится формальностью. Оставляйте файл закрытым и повторяйте проверку либо отправляйте его на разбор.
- Общий временный каталог переполнен. Файлы, которые не успели обработать, накапливаются. Нужны квота, срок удаления и метрика объёма временного хранилища.
- Доступ проверяется только при загрузке. Это не защищает от подмены идентификатора при скачивании. Проверяйте владельца или право доступа при каждой выдаче.
- Публичная ссылка живёт вечно. Её могут переслать или обнаружить в истории и логах. Используйте контролируемую выдачу или ограниченные по времени ссылки.
В проде следите за долей отказов проверки, временем от загрузки до решения, размером очереди и свободным местом во временном хранилище. Отдельно тестируйте сбой сканера, повторную отправку одного файла и ситуацию, когда пользователь теряет доступ между загрузкой и скачиванием. Эти проверки покажут, где система открывает файл раньше времени или не умеет безопасно восстановиться.
Что сделать дальше
Начните с инвентаризации всех точек загрузки: формы поддержки, аватары, импорты, интеграции и прямые ссылки на хранилище. Для каждой запишите разрешённые форматы, лимиты, место карантина и правило выдачи. Затем проверьте три сценария: подмена расширения, недоступность сканера и попытка скачать чужой файл. Если они не приводят к публикации или утечке, переходите к нагрузочному тесту очереди и очистки временных данных.
Ориентир по проверкам загрузок — OWASP File Upload Cheat Sheet. Материал описывает общую схему для веб-приложений и не заменяет проверку ограничений конкретного фреймворка, хранилища и версии прокси; перед внедрением сверяйте настройки с документацией используемых версий.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: OWASP File Upload Cheat Sheet, RFC 7578: Returning Values from Forms — multipart/form-data
Частые вопросы
Достаточно ли проверить расширение и MIME-тип?
Можно ли сразу сохранять загрузки в публичную папку?
Антивирус гарантирует, что файл безопасен?
Как безопасно выдавать файл владельцу?
Что делать, если проверка файла зависла или сканер недоступен?
Нужно ли отдельно ограничивать архивы?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.