Перейти к основному содержимому

Загрузка файлов и валидация в PHP

Разработчику Архитектору

Браузер отправляет файл в теле запроса multipart/form-data. PHP кладёт метаданные в $_FILES, тело — во временный каталог. Дальше разработчик проверяет файл и переносит его в хранилище через move_uploaded_file().

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

См. также данные со страницы, суперглобальные массивы — $_FILES, безопасные пути при работе с файлами, Laravel — Form Request.


Форма и лимиты сервера

<form method="POST" action="/upload.php" enctype="multipart/form-data">
<input type="file" name="document" accept=".pdf,application/pdf" required />
<button type="submit">Загрузить</button>
</form>

Разбор:

  • method="POST" отправляет файл в теле HTTP-запроса, а не в строке URL.
  • enctype="multipart/form-data" обязателен для передачи бинарных частей и метаданных файла.
  • Поле type="file" с name="document" формирует ключ $_FILES['document'] на сервере.
  • accept=".pdf,application/pdf" ограничивает выбор в интерфейсе браузера, но не является защитой.
  • required предотвращает пустую отправку на клиенте, при этом серверная проверка остается обязательной.
Настройка php.iniНазначение
upload_max_filesizeМаксимальный размер одного файла
post_max_sizeЛимит всего POST (должен быть ≥ суммы полей и файлов)
max_file_uploadsСколько файлов за один запрос
file_uploadsВключена ли загрузка (On / Off)

Код ошибки в $_FILES['поле']['error'] — константы UPLOAD_ERR_OK, UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE и др. Всегда проверяйте error === UPLOAD_ERR_OK до любой обработки содержимого.


Структура $_FILES

КлючОткуда берётсяМожно ли доверять
nameИмя на компьютере пользователяТолько для отображения; в пути на диске — своё имя
typeЗаголовок от браузераПодделывается
sizeРазмер в байтахОриентир; дополнительно проверяйте на диске
tmp_nameПуть к временному файлу на сервереИспользуйте с is_uploaded_file()
errorКод результата загрузкиОбязателен к проверке

Подробнее о суперглобальных — Глобальные переменные и суперглобальные массивы в PHP.


Что проверять (слои защиты)

ШагДействие
1REQUEST_METHOD === 'POST', поле присутствует, error === UPLOAD_ERR_OK
2Лимит размера в коде (меньше или равен политике приложения)
3MIME по содержимомуfinfo_file() / класс finfo, не $_FILES['type']
4Расширение — вспомогательный признак, не единственный
5is_uploaded_file($tmp) перед move_uploaded_file()
6Сохранение под случайным именем; оригинал — в БД при необходимости
7Каталог вне public/ или раздача через контроллер, не прямой URL на uploads/evil.php

Переименованный malware.exemalware.pdf пройдёт проверку только по расширению или по type из формы. Содержимое останется исполняемым.

Поэтому надежная политика строится как набор одновременно работающих ограничений, а не как одна "волшебная" проверка.


Валидация на чистом PHP

Код ITЗагрузка примера кода…

Разбор:

  • declare(strict_types=1); включает строгую типизацию для функций и параметров файла.
  • detectMime() использует finfo(FILEINFO_MIME_TYPE) и определяет тип по содержимому байтов, а не по имени.
  • Проверка $_SERVER['REQUEST_METHOD'] !== 'POST' отсекает неверный HTTP-метод ранним ответом 405.
  • $_FILES['document'] читается безопасно через null-coalescing и проверяется по UPLOAD_ERR_OK.
  • Ограничение size и is_uploaded_file($tmp) защищает от подмены пути и слишком больших файлов.
  • Имя хранения строится как bin2hex(random_bytes(16)) . '.pdf', что исключает коллизии и path traversal через исходное имя.
  • move_uploaded_file($tmp, $target) выполняет атомарный перенос только для действительно загруженных файлов.

Для изображений дополнительно имеет смысл getimagesize() — убедиться, что файл действительно декодируется как картинка.


Расширение и MIME — в чём разница

ПроверкаЧто смотритРиск
Расширение в имени (report.pdf)Суффикс строки nameПереименование любого файла
$_FILES['type']Заголовок от клиентаПодделка в DevTools
finfo / mime в фреймворкеСигнатура байтов на сервереНадёжнее; для PDF иногда нужны доп. проверки (парсер, антивирус)

Политика "только PDF" = MIME по содержимому + ограничение размера + безопасное хранение. Расширение согласуют с MIME, а не подменяют им.


Laravel — extensions, mimes и file

В Laravel валидация в $request->validate() или Form Request:

// Слабо: только суффикс имени
$request->validate([
'document' => 'required|extensions:pdf',
]);
// malware.exe, переименованный в malware.pdf → проходит

// Надёжнее: загруженный файл + MIME + расширение
$request->validate([
'document' => 'required|file|extensions:pdf|mimes:pdf',
]);
// тот же переименованный exe → отклоняется

Разбор:

  • Первый пример с extensions:pdf проверяет только суффикс имени и пропускает переименованные исполняемые файлы.
  • Второй пример добавляет file и mimes:pdf, что включает серверную проверку содержимого.
  • required гарантирует, что поле присутствует и не пустое.
  • Комбинация extensions + mimes дает одновременно UX-ограничение и фактическую валидацию по байтам.
  • В результате exe, переименованный в .pdf, отклоняется по MIME и не доходит до слоя хранения.
ПравилоСмысл
fileПоле — реальная загрузка через HTTP
extensions:pdfСуффикс имени в списке
mimes:pdfMIME по содержимому (как у finfo)
max:5120Размер в килобайтах
image, dimensionsДополнительно для картинок

Сохранение — $request->file('document')->store('documents') или диск Storage. Каталог storage/app по умолчанию недоступен из веба; публичная раздача — через php artisan storage:link и осознанный выбор файлов.

Form Request с файлом:

public function rules(): array
{
return [
'document' => ['required', 'file', 'mimes:pdf', 'max:5120'],
];
}

Разбор:

  • Метод rules() в Form Request централизует правила и делает валидацию повторно используемой.
  • required и file проверяют обязательность и тип входа как загруженного файла.
  • mimes:pdf разрешает только PDF по содержимому, а не по заголовку клиента.
  • max:5120 ограничивает размер до 5120 КБ и защищает приложение от чрезмерной нагрузки.
  • Возврат массива правил удобно покрывать тестами отдельно от контроллера.

Symfony

В Symfony 6+ для загрузок используют ограничение File / Image в валидаторе — maxSize, mimeTypes, extensions. Логика та же — проверка на сервере по содержимому и размеру, не по доверию к имени файла. Обзор фреймворка — Symfony.


Типовые ошибки

ОшибкаПоследствие
Сохранять под оригинальным name в public/uploadsPath traversal, выполнение .php
Доверять только accept в HTMLАтрибут только подсказка браузеру
Пропустить is_uploaded_fileЗапись произвольного пути с диска
Хранить исполняемые типы в каталоге, откуда веб-сервер исполняет PHPRCE при обходе фильтра

Частая архитектурная ошибка: считать загрузку "завершенной" сразу после move_uploaded_file(). Обычно нужен второй шаг:

  • запись метаданных в БД (owner_id, mime, size, stored_name, created_at);
  • постановка фоновой проверки (антивирус, генерация превью, извлечение текста);
  • публикация файла только после прохождения проверки.

Связанные материалы


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.