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

> Загрузка файлов на PHP: почему расширение и Content-Type не защита, как проверить тип через finfo и куда сохранять файлы безопасно.

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

Проверка вида 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](https://www.zevshost.net/ru/kb/php-logi-oshibok-razbor/).

## Ограничение размера: 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](https://www.zevshost.net/ru/kb/php-ini-memory-upload-limits/). Второй уровень — проверка $\_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 через настройки](https://www.zevshost.net/ru/kb/php-security-hardening-ini/).

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

- Доверие расширению или 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 перед сохранением.

---

Источник: https://www.zevshost.net/ru/kb/php-zagruzka-faylov-bezopasnost/
