К основному содержимому

Загрузка файлов на PHP: проверка типа и защита

PHP · 29.09.2026

Почему проверка расширения файла не защищает от загрузки чужого кода

Проверка вида if (strpos($_FILES['file']['name'], '.php') !== false) ничего не решает: злоумышленник переименует shell.php в shell.php.jpg, а на некоторых конфигурациях Apache с mod_mime сервер всё равно выполнит его как PHP из-за составного расширения. Расширение файла — это метаданные, которые полностью контролирует тот, кто отправляет форму, а не признак реального содержимого.

То же самое касается заголовка Content-Type из $_FILES['file']['type']: браузер подставляет его на основе расширения на стороне клиента, и curl или Postman позволяют указать любое значение вручную. Доверять этому полю в проверках безопасности нельзя вообще.

Проверка реального типа файла через finfo

Единственный надёжный способ — прочитать сигнатуру файла и определить MIME-тип по содержимому, а не по имени или заголовку запроса.

$finfo = new finfo(FILEINFO_MIME_TYPE);
$realType = $finfo->file($_FILES['avatar']['tmp_name']);

$allowed = ['image/jpeg', 'image/png', 'image/webp'];
if (!in_array($realType, $allowed, true)) {
    throw new RuntimeException('Недопустимый тип файла: ' . $realType);
}

Проверка идёт по временному файлу $_FILES['avatar']['tmp_name'], который PHP уже сохранил на диск после загрузки, — именно его содержимое и нужно анализировать, а не оригинальное имя файла. Исключение RuntimeException стоит логировать, а не показывать пользователю напрямую — принципы разобраны в статье о логах и разборе фаталов PHP.

Ограничение размера: php.ini и код

Размер загружаемого файла ограничивается на двух уровнях. Сначала — php.ini, до того как скрипт вообще получит управление.

upload_max_filesize = 10M
post_max_size = 12M

Значение post_max_size должно быть больше upload_max_filesize, иначе PHP обрежет весь запрос ещё до того, как разберёт файлы, и $_FILES окажется пустым без явной ошибки. Подробный разбор этих директив и типовых ошибок — в статье про memory_limit и upload_max_filesize. Второй уровень — проверка $_FILES['avatar']['size'] в коде до вызова move_uploaded_file, если бизнес-логике нужен лимит меньше, чем в php.ini.

Куда сохранять загруженные файлы

ПравилоПлохоХорошо
Расположениевнутри веб-корня /publicвне веб-корня, отдаётся скриптом
Имя файлаоригинальное имя от пользователяслучайное имя через random_bytes
Права доступа0777 на каталог загрузок0755 на каталог, 0644 на файлы
ИсполнениеPHP разрешён в каталоге загрузокphp_admin_flag engine off в конфиге

Если каталог загрузок физически лежит внутри веб-корня, добавьте в конфигурацию php_admin_flag engine off именно для этого пути — тогда даже загруженный файл с расширением .php выполнится не как код, а отдастся как обычный текст. Общие принципы усиления php.ini собраны в статье о безопасности PHP через настройки.

Частые ошибки при приёме файлов

  • Доверие расширению или Content-Type из запроса вместо проверки содержимого через finfo.
  • Сохранение файла под оригинальным именем — путь вида ../../config.php в имени файла даёт запись за пределы каталога загрузок.
  • Отсутствие ограничения на количество файлов в одном запросе — это открывает почву для DoS через тысячи мелких загрузок.
  • move_uploaded_file вызывается без предварительной проверки is_uploaded_file, из-за чего скрипт можно обмануть подложным путём.
  • Загруженные изображения не перекодируются через GD или Imagick — вредоносный код можно спрятать внутри валидного на вид JPEG.

Итог: чек-лист безопасной загрузки файлов

  • Реальный тип файла проверяется через finfo по содержимому, а не по расширению или Content-Type.
  • upload_max_filesize и post_max_size заданы в php.ini, лишний лимит по размеру проверяется в коде.
  • Файлы сохраняются под случайным именем вне веб-корня или с отключённым исполнением PHP.
  • Каталог загрузок имеет права 0755/0644, а не 0777.
  • Изображения проходят перекодировку через GD или Imagick перед сохранением.
← Назад в базу знаний Задать вопрос поддержке