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

Загрузка файлов и валидация в 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>
Настройка 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Код результата загрузкиОбязателен к проверке

Подробнее о суперглобальных — 154.md.


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

ШагДействие
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

<?php

declare(strict_types=1);

function detectMime(string $path): string
{
$finfo = new finfo(FILEINFO_MIME_TYPE);
return $finfo->file($path) ?: 'application/octet-stream';
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit('Method Not Allowed');
}

$file = $_FILES['document'] ?? null;

if ($file === null || $file['error'] !== UPLOAD_ERR_OK) {
http_response_code(400);
exit('Ошибка загрузки');
}

$tmp = $file['tmp_name'];
$maxBytes = 5 * 1024 * 1024;
$allowedMimes = ['application/pdf' => 'pdf'];

if ($file['size'] > $maxBytes || !is_uploaded_file($tmp)) {
http_response_code(400);
exit('Файл отклонён');
}

$mime = detectMime($tmp);
if (!isset($allowedMimes[$mime])) {
http_response_code(400);
exit('Допустим только PDF');
}

$extension = $allowedMimes[$mime];
$storedName = bin2hex(random_bytes(16)) . '.' . $extension;
$uploadDir = __DIR__ . '/../var/uploads';
$target = $uploadDir . '/' . $storedName;

if (!is_dir($uploadDir) && !mkdir($uploadDir, 0750, true)) {
http_response_code(500);
exit('Каталог недоступен');
}

if (!move_uploaded_file($tmp, $target)) {
http_response_code(500);
exit('Не удалось сохранить файл');
}

// $file['name'] — только для UI или записи в БД как "оригинальное имя"

Для изображений дополнительно имеет смысл 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 → отклоняется
ПравилоСмысл
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'],
];
}

Symfony (кратко)

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


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

ОшибкаПоследствие
Сохранять под оригинальным 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 как основа веб-интеграций.

См. также

Другие статьи этого же раздела в боковом меню (как на странице "О разделе").