Загрузка файлов на сервер через PHP: короткий гайд
Форма с input type=file, обработчик на PHP и проверка результата. Через 20 минут у вас работающая загрузка с валидацией размера и типа файла — без дыр вроде записи в корень сайта.

К концу этого руководства у вас будет HTML-форма, PHP-обработчик и папка, куда безопасно складываются загруженные файлы. Проверено на PHP 8.2 и 8.3. Займёт минут двадцать, если PHP уже установлен и работает. Загрузка файлов — одно из мест, где новичок легче всего оставляет дыру: принимает файл без проверки типа, кладёт его в директорию, откуда PHP потом его выполнит. Мы это закроем сразу.
Что нужно иметь до начала
- PHP 8.1 или новее. Проверьте командой
php -v— версия печатается в первой строке. - Веб-сервер, который отдаёт PHP: встроенный сервер PHP, Apache или Nginx с PHP-FPM. Для теста хватит встроенного.
- Доступ на запись в папку проекта, чтобы создать директорию для файлов.
- Права на редактирование
php.iniили знание текущих лимитов — о них ниже.
Если ничего из этого нет под рукой, самый быстрый вариант для теста — запустить встроенный сервер из папки проекта командой php -S localhost:8000. Он подходит только для разработки, не для боевого сайта.
Одно важное про лимиты
PHP по умолчанию ограничивает размер загрузки двумя директивами в php.ini: upload_max_filesize и post_max_size. Второй должен быть не меньше первого, иначе крупный файл обрежется на уровне POST-запроса, и вы получите пустой $_FILES без понятной ошибки. Значения по умолчанию небольшие (обычно 2M и 8M), так что если грузите что-то тяжелее — поднимите оба.
Шаг 1. HTML-форма
Создайте файл index.html. Ключевой момент — атрибут enctype="multipart/form-data". Без него браузер отправит только имя файла, а не его содержимое, и $_FILES окажется пустым.
<!DOCTYPE html>
<html lang="ru">
<head><meta charset="utf-8"><title>Загрузка</title></head>
<body>
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="document">
<button type="submit">Загрузить</button>
</form>
</body>
</html>
Проверка: откройте http://localhost:8000/index.html — должна появиться кнопка выбора файла и кнопка «Загрузить».
Шаг 2. Папка для файлов
Рядом с PHP-файлами создайте директорию uploads. Складывать загруженное в корень сайта — плохая идея: файл станет доступен по прямой ссылке, а если это .php, сервер его выполнит. Держите директорию отдельно и, в идеале, вне корня публичных файлов.
mkdir uploads
Проверка: команда ls -la должна показать папку uploads с правами на запись у пользователя, от которого работает PHP.
Шаг 3. Обработчик upload.php
Здесь основная работа. Разберём построчно, что делает каждая проверка, а полный рабочий файл — ниже.
$_FILES['document']['error']— код ошибки загрузки.UPLOAD_ERR_OK(0) означает успех. Другие коды: превышен лимит, файл пришёл частично, папка недоступна.is_uploaded_file()— подтверждает, что файл действительно пришёл через HTTP-загрузку, а не подсунут по абсолютному пути. Это защита от подмены.finfo— определяет реальный MIME-тип по содержимому. Не доверяйте$_FILES['document']['type']: это значение присылает браузер, его легко подделать.move_uploaded_file()— переносит файл из временной директории в вашу. Именно эта функция, а неrename()илиcopy(): она дополнительно проверяет, что файл загруженный.
<?php
// PHP 8.1+
declare(strict_types=1);
$targetDir = __DIR__ . '/uploads/';
$maxSize = 5 * 1024 * 1024; // 5 МБ
// разрешённые типы: реальный MIME => расширение
$allowed = [
'image/jpeg' => 'jpg',
'image/png' => 'png',
'application/pdf' => 'pdf',
];
// 1. Файл вообще пришёл?
if (!isset($_FILES['document']) || $_FILES['document']['error'] === UPLOAD_ERR_NO_FILE) {
exit('Файл не выбран');
}
$file = $_FILES['document'];
// 2. Код ошибки загрузки
if ($file['error'] !== UPLOAD_ERR_OK) {
exit('Ошибка загрузки, код: ' . $file['error']);
}
// 3. Реальная загрузка, а не подмена пути
if (!is_uploaded_file($file['tmp_name'])) {
exit('Недопустимый источник файла');
}
// 4. Размер
if ($file['size'] > $maxSize) {
exit('Файл больше 5 МБ');
}
// 5. Реальный тип по содержимому
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
if (!isset($allowed[$mime])) {
exit('Недопустимый тип файла: ' . $mime);
}
// 6. Своё имя вместо присланного
$ext = $allowed[$mime];
$newName = bin2hex(random_bytes(16)) . '.' . $ext;
$targetPath = $targetDir . $newName;
// 7. Перенос
if (move_uploaded_file($file['tmp_name'], $targetPath)) {
echo 'Загружено: ' . htmlspecialchars($newName);
} else {
exit('Не удалось сохранить файл');
}
Обратите внимание на шаг 6: имя файла генерируется заново через random_bytes(), а расширение берётся из белого списка, а не из присланного имени. Так вы отсекаете имена вроде shell.php.jpg и путаницу с кириллицей и пробелами.
Проверка: выберите картинку JPG или PNG, нажмите «Загрузить» — должна появиться строка «Загружено: » со случайным именем. Файл появится в папке uploads. Попробуйте загрузить .txt или .php — увидите «Недопустимый тип файла».
Никогда не доверяйте расширению и MIME-типу, которые прислал браузер. Единственный источник правды о содержимом — сам байтовый поток файла, прочитанный через finfo. Всё остальное подделывается за пять секунд.
Шаг 4. Запретите выполнение в папке uploads
Даже с белым списком типов защита не полная: закройте выполнение скриптов в директории загрузок на уровне сервера. На Apache положите в uploads/.htaccess:
php_flag engine off
<FilesMatch "\.(php|phtml|phar)$">
Require all denied
</FilesMatch>
Для Nginx .htaccess не работает — там нужно правило в конфиге сервера, запрещающее передачу файлов из uploads в PHP-FPM. Точный синтаксис location зависит от вашей конфигурации, сверьтесь с документацией Nginx по обработке location.
Если не получилось
- $_FILES пустой, ошибок нет. Скорее всего забыт
enctype="multipart/form-data"в форме или файл большеpost_max_size. Проверьтеphp -i | grep post_max_size. - Код ошибки 1 или 2 в поле error. Файл превысил
upload_max_filesize(код 1) или лимит формы (код 2). Поднимите директивы вphp.iniи перезапустите PHP-FPM или веб-сервер. - «Не удалось сохранить файл». У процесса PHP нет прав на запись в
uploadsлибо папки не существует. Проверьте владельца и права:ls -la, при необходимостиchownна пользователя веб-сервера. - Class "finfo" not found. Не подключено расширение fileinfo. В большинстве сборок оно включено по умолчанию; проверьте
php -m | grep fileinfoи включите вphp.ini. - Файл загрузился, но открывается битым. Обычно причина — вывод чего-либо (пробел, BOM, эхо) до заголовков или до чтения потока. Убедитесь, что в начале
upload.phpнет пустых строк перед<?php.
Что делать дальше
Рабочая база готова. Дальше по мере роста проекта смотрите в сторону: загрузки нескольких файлов через name="documents[]" (тогда $_FILES станет массивом массивов — структура отличается, это отдельная тема); ограничения по количеству загрузок с одного IP; хранения метаданных о файле в базе, а не только на диске. Если файлы большие или их много — переносите хранилище в объектное хранилище (S3-совместимое) вместо локального диска, чтобы не упереться в место и не потерять данные при масштабировании.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: PHP Manual: Handling file uploads, PHP Manual: move_uploaded_file
Частые вопросы
А если у меня PHP 7.4, код подойдёт?
Почему нельзя использовать $_FILES['document']['type'] для проверки типа?
Загрузка работает локально, но на боевом сервере файл не сохраняется — почему?
Как разрешить загрузку файлов больше 2 МБ?
Достаточно ли белого списка типов, чтобы не загрузили вредоносный скрипт?
Как обработать загрузку нескольких файлов сразу?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.