До разработки согласуйте допустимые типы и размеры, число файлов, роли отправителей и получателей, место хранения, обязательные проверки и понятные статусы. Затем проверьте весь маршрут: от выбора файла до удаления записи и связанных копий. Учебный пример ниже показывает локальный приём синтетического PDF, но не заменяет проверку безопасности реального сервиса.

Начать разбор

Сначала определите путь файла

У вложения есть несколько разных этапов, и каждый должен иметь отдельный результат. «Отправлено», «сервер принял байты» и «файл доступен получателю» — не одно и то же.

ЭтапЧто должно происходитьЧто проверить
ВыборИнтерфейс объясняет, какие вложения нужныДопустимые типы, размер и количество видны до выбора
ПередачаСервер принимает запрос в пределах настроенных лимитовОграничения приложения, PHP, веб-сервера и прокси не противоречат друг другу
ПроверкаСервер применяет правила к каждому файлуРасширение, фактический тип, размер, права, бизнес-контекст
Ожидание проверкиФайл закрыт от обычной выдачи, пока не завершены нужные проверкиСостояние, причина ожидания и способ повторной обработки записаны
ДоступВложение связано с записью и доступно только разрешённой ролиПользователь не может подменить идентификатор и получить чужой файл
Повтор или удалениеНеопределённый результат можно проверить; удаление учитывает связанные копииСтатус по идентификатору, политика хранения, файлы и метаданные удалены согласованно

Этот список не задаёт обязательную инфраструктуру. Например, карантин и автоматическое сканирование имеют смысл, когда они соответствуют риску, типу файлов и возможностям команды. OWASP рекомендует строить защиту слоями: ни один отдельный фильтр не решает все проблемы приёма и выдачи файлов. OWASP File Upload Cheat Sheet

Согласуйте ограничения до реализации

Не начинайте с вопроса «какой максимальный размер поставить?». Сначала выясните, какие файлы нужны процессу, кто их отправляет, что происходит с вложением после получения и сколько ресурсов доступно для хранения и проверки. Затем внесите параметры в одну матрицу требований.

ПараметрЧто решитьКак принять результат
ФорматыКакие конкретные типы действительно нужныДля каждого формата есть разрешённое расширение, ожидаемый тип содержимого и сценарий обработки
Размер одного файлаКакой верхний предел нужен задачеПревышение даёт объяснимый отказ, а лимиты компонентов согласованы
Размер всего запросаСколько данных может прийти за одну отправкуУчтены все файлы, поля формы и служебные данные запроса
Число вложенийОдин файл или несколько, допустимы ли пустые позицииПроверены лимит приложения и обработка каждого элемента
ОтправительГость, авторизованный пользователь, сотрудник, интеграцияКаждая роль может только начать разрешённый сценарий
ПолучательКто может просматривать и скачивать результатПроверка прав привязана к владельцу и бизнес-записи, а не только к известному URL
Хранение и удалениеКогда файл перестаёт быть нужным и где ещё есть его копииОписаны событие удаления, связанные метаданные и резервные/производные копии

Примеры значений в такой таблице — предметное решение, а не универсальные нормы. Не копируйте чужой лимит в настройки своего сервера без проверки тарифа, конфигурации PHP, веб-сервера, прокси и фактического размера запроса.

Ирбис сопоставляет файлы с ограничениями по размеру и проверяет их содержимое
Иллюстрация к матрице форматов и лимитов; значения ограничений нужно определить для конкретной системы.

Почему `accept` не проверяет файл

Атрибут accept у <input type="file"> помогает файловому диалогу показать подходящие варианты. Он не доказывает, что выбранное содержимое соответствует расширению, и не ограничивает запрос на стороне сервера. Пользовательский интерфейс может быть обойдён, а клиентские имя и MIME-тип не являются надёжным источником истины. MDN отдельно указывает, что ожидаемое требование нужно проверять на сервере. MDN: accept

Поэтому проверку строят на стороне приложения и сверяют несколько признаков с ожидаемым сценарием. OWASP советует разрешать только необходимые типы, не доверять заголовку Content-Type, генерировать имя хранения на сервере, ограничивать размер, проверять права и хранить загрузки в защищённом месте. Проверка сигнатуры файла может быть дополнительным слоем, но не должна использоваться в одиночку. OWASP: File Upload Protection

Практически это означает:

  • определите короткий allowlist по задаче, а не блокируйте несколько известных опасных расширений, оставляя всё остальное;
  • проверьте имя и расширение после нормализации, но не используйте исходное имя как путь на диске;
  • проверяйте размер на сервере и контролируйте число файлов;
  • сопоставляйте заявленный формат с тем, что сервер обнаруживает при разборе;
  • не запускайте загруженное содержимое и не передавайте его обработчику до нужных проверок;
  • выдавайте вложение через маршрут, который каждый раз проверяет право пользователя;
  • если сканирование или преобразование отсутствуют, не показывайте файл как проверенный; для загрузки через браузерную сессию отдельно предусмотрите защиту от CSRF по модели вашего приложения.

Для обработки изображений, архивов и офисных документов добавляются отдельные риски парсеров и распаковки. Выбирайте проверенные актуальные библиотеки, ограничивайте ресурсы обработки и не раскрывайте файлы внешнему сервису сканирования без оценки конфиденциальности. Даже антивирусная проверка не должна описываться как абсолютная гарантия безопасности.

Сверьте лимиты всех уровней

В PHP размер одного файла ограничивает upload_max_filesize, а post_max_size должен быть больше, поскольку относится к запросу целиком. На количество одновременных файлов влияет max_file_uploads; время получения входных данных ограничивается отдельно. Эти параметры описаны в официальном руководстве PHP. PHP: core upload directives и PHP: common upload pitfalls

Но приложение — только один уровень. Перед тестом зафиксируйте эффективные лимиты и на других слоях: reverse proxy или CDN, веб-сервер, PHP, код обработки и хранилище. Если самый внешний слой отвергает запрос раньше PHP, приложение может не получить ожидаемую запись об ошибке. Если лимит файлов или времени различается между компонентами, пользователь увидит неожиданный отказ или запрос завершится без ожидаемого статуса.

Сделайте таблицу согласования конфигурации и проведите граничные проверки вокруг ваших значений: файл ниже предела, ровно на пределе, выше предела; несколько файлов суммарно больше ограничения запроса; пустой файл; медленная передача; заполненное или недоступное хранилище. Для каждой границы задайте ожидаемый HTTP-ответ или статус, запись в журнале и действие пользователя. Не пытайтесь компенсировать неверный лимит скрытым полем MAX_FILE_SIZE: значение из формы контролирует клиент, поэтому решающее ограничение должно оставаться серверным.

Дайте ошибке точное состояние

В PHP состояние передачи доступно в $_FILES[field][error] и представлено константами UPLOAD_ERR_*. В руководстве перечислены разные причины, в том числе превышение настроенного предела, частичная передача, отсутствие временного каталога, невозможность записи и остановка загрузки расширением. Их нельзя сводить к сообщению «что-то пошло не так»: оператору нужен технический код, а пользователю — следующий понятный шаг. PHP: Error Messages Explained

Например, «Файл больше допустимого размера. Выберите файл до указанного ограничения» помогает исправить ввод. Сообщение «Не удалось временно сохранить файл. Попробуйте позже; если ошибка повторится, сообщите номер операции» подходит для инфраструктурного сбоя. Точный путь на сервере, стек ошибки и внутренние имена каталогов пользователю не нужны; они принадлежат журналу с ограниченным доступом.

Полезно предусмотреть состояния вроде received, validating, quarantined, available, rejected, failed и deleted. Это пример модели, не готовый стандарт. Названия можно изменить, если из системы однозначно понятно, что произошло, кому разрешён доступ и какое действие следует дальше.

СитуацияЧто известно системеСледующий шаг
Пользователь отменил выборФайл не выбранНе создавать запись о принятом вложении
Формат или размер не подходятЗапрос получен, правило нарушеноОтказать до выдачи, объяснить исправление
Передача оборваласьПолучен неполный запрос или код частичной передачиНе маркировать как принятый; предложить безопасный повтор
Временный каталог/диск недоступенСервер не завершил записьСохранить техническую причину в журнале, показать временный отказ
Ответ потерялся после приёмаКлиент не знает, сохранился ли файлПроверить статус операции перед повторной отправкой
Проверка ещё идётФайл принят, но решение не завершеноНе выдавать обычным получателям; показать ожидание и статус
Удаление завершеноФайл и связь с записью удалены по правиламНе оставлять доступную старую ссылку; учесть связанные копии по политике продукта

Самый коварный случай — тайм-аут после передачи. Пользователь видит, что страница не ответила, но по одному этому признаку нельзя определить, сохранил ли сервер файл. Если повторно отправить вложение вслепую, могут появиться дубликаты. До отправки создавайте идентификатор операции, а после сбоя сначала запрашивайте её состояние. Повтор должен быть идемпотентным: тот же ключ не создаёт второй объект, если первая операция уже завершилась. Это вывод из модели процесса и проверяется на конкретной системе, а не свойство, которое PHP включает автоматически.

Ирбис отслеживает статусы загрузки, повтор операции и отказ, не выдавая незавершённый файл
Иллюстрация состояний операции; конкретная система должна определять и журналировать каждый переход.

Разделите приём, хранение и выдачу

Не размещайте пользовательские вложения в публичной папке так, чтобы достаточно было угадать имя. OWASP рекомендует предпочитать отдельное хранилище или путь вне webroot; если файл нужно выдавать, запрос должен проходить через управляемый приложением идентификатор и проверку доступа. Имя, которое прислал клиент, не должно определять путь хранения. OWASP: File Storage Location and User Permissions

Для каждой записи определите, кто отправил файл, к какой сущности он относится, где находится его объект хранения, каков статус проверки и кто вправе его читать или удалить. При скачивании сервер должен найти запись по внутреннему идентификатору и проверить право текущего пользователя. Секретный URL без проверки личности — не замена авторизации, если ссылку можно переслать или подобрать.

Перед удалением решите, что именно означает команда «удалить»: скрыть файл от пользователя, удалить основной объект, стереть метаданные, отозвать ссылки или учесть копии в экспортах и интеграциях. Конкретный срок хранения зависит от задачи, договора и применимых правил. Здесь не приводится универсальный срок: его нужно подтвердить отдельно, включая поведение резервных копий и внешних систем.

Учебный пример: локальная загрузка без публичного доступа

В комплекте статьи — изолированный обработчик для 127.0.0.1, который принимает только синтетический PDF в пределах специально заданного учебного лимита. Он получает реальный multipart-запрос по локальной сети, проверяет успешный статус передачи в PHP, размер и сигнатуру %PDF-, присваивает случайный идентификатор, сохраняет объект вне webroot, а затем проверяет статус операции. Повтор с тем же ключом не создаёт вторую запись. Примеры отказа покрывают неподдерживаемое расширение, несовпадение типа, превышение тестового ограничения и неизвестный идентификатор операции.

Это узкая проверка маршрута, а не законченный сервис вложений. В примере нет production-аутентификации, антивируса, проверки сложных PDF, конкурирующих запросов, ограничения на reverse proxy и интеграции с реальным хранилищем. В него не загружаются персональные или вредоносные файлы. Не используйте демонстрационный сервер в интернете и не переносите фиксированный лимит в рабочий проект.

Скачать комплект учебного примера. Сценарий запуска и ожидаемые результаты находятся в README внутри архива. Он создаёт временное закрытое хранилище и синтетический документ, отправляет запрос только на loopback, сверяет состояние и очищает временную директорию после теста. Так можно повторить учебный эксперимент, не приписывая ему проверку сайта клиента или production-защиты.

Чек-лист перед разработкой и приёмкой

До начала работ ответьте на вопросы:

  1. Какие конкретные форматы нужны процессу, сколько файлов и какого предельного размера?
  2. Какие ограничения выставлены на клиенте, приложении, PHP, веб-сервере, прокси и хранилище?
  3. Где проверяются фактический тип и допустимость содержимого, и что происходит до завершения проверки?
  4. Как связаны файл, отправитель, заявка или пользовательская запись?
  5. Кто может загрузить, просмотреть, скачать, заменить и удалить вложение?
  6. Что увидит пользователь при превышении лимита, частичной передаче, временном сбое и неизвестном результате?
  7. Как клиент проверяет статус и безопасно повторяет незавершённую операцию?
  8. Где хранится исходное имя, какие данные попадут в журнал и кто увидит технические ошибки?
  9. Какой процесс удаления действует для файла, метаданных, доступных ссылок и связанных копий?
  10. Какими безвредными тестовыми файлами проверяются допустимый формат, отказ, границы размера, права, повтор и удаление?

Принимайте не только экран загрузки. Проверьте получение и хранение, понятные отказы, изоляцию от публичной выдачи, проверку прав при скачивании, повтор после тайм-аута и согласованное удаление. Если нужен файл, который безопасно проходит обработку и становится доступен, каждое из этих состояний должно быть наблюдаемым для команды.

Если вы проектируете кабинет, сервисную интеграцию или нестандартную обработку вложений, команда Бадрик Лаб может помочь описать сценарии и реализовать их в рамках доработки и интеграций сайта. Сначала нужно определить типы данных, роли и требования к хранению; точную схему и состав работ согласуют после изучения задачи.

Источники и документы

Автор статьи

Основатель Бадрик Лаб и веб-разработчик. Проектирует и запускает сайты, отвечает за техническую архитектуру, приёмку и развитие проектов студии.