До основного вмісту

Завантаження файлів на 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 перед збереженням.
← Назад до бази знань Поставити питання підтримці