# Завантаження файлів на 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/uk/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/uk/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/uk/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/uk/kb/php-zagruzka-faylov-bezopasnost/
