Загрузка файлов в PHP: пошаговое руководство с безопасностью и примерами
Запрос «загрузка файлов в PHP» чаще всего приводит к примерам на пару строк, но в продакшене так делать нельзя. Ниже — пошаговое руководство с акцентом на безопасность: форма, приём через $_FILES, валидация MIME, ограничение размеров, безопасное хранилище, множественная загрузка и частые ошибки.
1) HTML‑форма для загрузки файла
Обязательные атрибуты: method="post" и enctype="multipart/form-data". Добавим CSRF‑токен и ограничитель размера на клиенте (не полагайтесь на него полностью).
<?php
session_start();
if (empty($_SESSION['csrf'])) {
$_SESSION['csrf'] = bin2hex(random_bytes(32));
}
?>
<form action="upload.php" method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf" value="<?= htmlspecialchars($_SESSION['csrf'], ENT_QUOTES) ?>">
<!-- Ограничитель для браузера (сервер всё равно проверяет сам) -->
<input type="hidden" name="MAX_FILE_SIZE" value="2097152"> <!-- 2 МБ -->
<input type="file" name="file" accept="image/png,image/jpeg,image/webp,application/pdf" required>
<button type="submit">Загрузить</button>
</form>
2) Базовая обработка на сервере (безопасно)
Ключевые идеи: не доверять расширению и $_FILES['type'], проверять MIME через finfo_file, задавать лимиты размера, переименовывать файл случайным именем и хранить вне web‑корня.
<?php
declare(strict_types=1);
session_start();
if (!isset($_POST['csrf'], $_SESSION['csrf']) || !hash_equals($_SESSION['csrf'], $_POST['csrf'])) {
http_response_code(403);
exit('Неверный CSRF-токен');
}
// 1) Проверяем наличие файла и ошибки PHP
if (!isset($_FILES['file'])) {
exit('Файл не получен');
}
$err = $_FILES['file']['error'];
if ($err !== UPLOAD_ERR_OK) {
$map = [
UPLOAD_ERR_INI_SIZE => 'Размер превысил upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'Размер превысил MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'Файл загружен частично',
UPLOAD_ERR_NO_FILE => 'Файл не выбран',
UPLOAD_ERR_NO_TMP_DIR => 'Нет временной директории',
UPLOAD_ERR_CANT_WRITE => 'Ошибка записи на диск',
UPLOAD_ERR_EXTENSION => 'PHP-расширение остановило загрузку',
];
exit($map[$err] ?? 'Неизвестная ошибка загрузки');
}
// 2) Серверные лимиты размера
$maxSize = 2 * 1024 * 1024; // 2 МБ
if ($_FILES['file']['size'] > $maxSize) {
exit('Слишком большой файл (макс. 2 МБ)');
}
// 3) Разрешённые типы и расширения
$allowed = [
'png' => 'image/png',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'webp' => 'image/webp',
'pdf' => 'application/pdf',
];
// Исходное имя используется только для получения расширения
$originalName = $_FILES['file']['name'];
$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!array_key_exists($ext, $allowed)) {
exit('Недопустимое расширение файла');
}
// 4) Проверка реального MIME с помощью Fileinfo
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['file']['tmp_name']);
if ($mime !== $allowed[$ext]) {
exit('Несоответствие MIME-типа');
}
// Доп. проверка для изображений (защита от подмены)
if (str_starts_with($mime, 'image/')) {
$imgSize = @getimagesize($_FILES['file']['tmp_name']);
if ($imgSize === false) {
exit('Файл не является валидным изображением');
}
}
// 5) Хранилище вне web-корня
$storage = dirname(__DIR__) . '/storage/uploads'; // ../storage/uploads
if (!is_dir($storage)) {
if (!mkdir($storage, 0775, true) && !is_dir($storage)) {
exit('Не удалось создать директорию для загрузок');
}
}
// 6) Случайное имя файла
$basename = bin2hex(random_bytes(16));
$target = $storage . '/' . $basename . '.' . $ext;
// 7) Безопасное перемещение
if (!is_uploaded_file($_FILES['file']['tmp_name'])) {
exit('Источник файла не является загруженным через HTTP');
}
if (!move_uploaded_file($_FILES['file']['tmp_name'], $target)) {
exit('Не удалось сохранить файл');
}
chmod($target, 0644); // минимально необходимые права
echo 'Файл успешно загружен! Идентификатор: ' . $basename;
Почему так безопаснее
- Хранение вне web‑корня исключает прямой HTTP‑доступ к загруженным файлам, даже если это скрипты.
- Проверка
finfo_fileзащищает от подмены расширения и заголовков. - Случайные имена без исходного названия предотвращают коллизии и утечку информации о пользователях.
- Жёсткие лимиты размера и типов уменьшают риск DoS и загрузки вредоносных файлов.
3) Множественная загрузка файлов
Во входных данных имена будут массивами. Логику проверки вынесем в функцию и применим в цикле.
<!-- форма -->
<form action="multi-upload.php" method="post" enctype="multipart/form-data">
<input type="file" name="files[]" multiple accept="image/png,image/jpeg">
<button>Загрузить</button>
</form>
<?php
function process_upload(array $file): string {
// Здесь можно переиспользовать логику из предыдущего примера
// ... проверки ошибок, MIME, размера и т.д.
// Вернём путь или ID сохранённого файла
return 'ok';
}
$files = $_FILES['files'] ?? null;
if (!$files) exit('Нет файлов');
$results = [];
for ($i = 0; $i < count($files['name']); $i++) {
$file = [
'name' => $files['name'][$i],
'type' => $files['type'][$i],
'tmp_name' => $files['tmp_name'][$i],
'error' => $files['error'][$i],
'size' => $files['size'][$i],
];
$results[] = process_upload($file);
}
print_r($results);
4) Настройка php.ini и сервера
- upload_max_filesize — максимальный размер одного файла (например, 5M).
- post_max_size — должен быть больше или равен upload_max_filesize (например, 6M).
- max_file_uploads — лимит количества файлов за один запрос (по умолчанию 20).
- file_uploads = On — разрешить загрузку файлов.
- max_input_time, memory_limit — косвенно влияют на большие загрузки.
- Nginx: client_max_body_size; Apache: LimitRequestBody — не забудьте согласовать с php.ini.
5) Как выдавать загруженные файлы безопасно
Если файлы лежат вне web‑корня, отдавайте их через контроллер, явно задавая заголовки и тип, без выполнения кода внутри файла.
<?php
// download.php?id=...
$id = preg_replace('/[^a-f0-9]/', '', $_GET['id'] ?? '');
$path = dirname(__DIR__) . '/storage/uploads/' . $id . '.pdf'; // например, для PDF
if (!is_file($path)) {
http_response_code(404);
exit('Файл не найден');
}
header('Content-Type: application/pdf');
header('Content-Length: ' . filesize($path));
header('Content-Disposition: inline; filename="document.pdf"');
readfile($path);
Храните метаданные в БД: формат, оригинальное имя, путь, размер, владелец, права доступа. Так вы сможете точно контролировать, что и кому отдаёте.
6) Частые ошибки и как их избежать
- Доверие к
$_FILES['type']— используйтеfinfo_fileи доп. проверки изображений. - Сохранение в доступной директории: поместите файлы вне web‑корня или отключайте исполнение скриптов там, где храните загрузки.
- Отсутствие переименования — коллизии, утечки путей и XSS через имя файла.
- Нет серверных лимитов размера — легко получить переполнение или DoS.
- Пропуск проверки ошибок PHP — файл может быть частично загружен.
- Игнорирование CSRF — злоумышленник может инициировать нежелательную загрузку от имени пользователя.
7) Мини‑чек‑лист безопасной загрузки
- Форма: POST + multipart, CSRF‑токен, разумный accept и клиентский лимит.
- Сервер: проверка кодов ошибок, размер, MIME через Fileinfo, доп. проверка изображений.
- Whitelist расширений и типов, случайные имена, права 0644.
- Хранение вне web‑корня, выдача через контроллер с корректными заголовками.
- Согласованные лимиты php.ini и веб‑сервера, обработка множественной загрузки.
Что дальше изучать
Для системного закрепления темы — работа с БД, авторизацией, MVC‑структурой, тестами и деплоем — рекомендую пройти практический курс: Пройти интенсивный курс «PHP и MySQL с Нуля до Гуру 3.0» и собрать проекты с нуля.
Теперь вы знаете не только как сделать «загрузчик на коленке», но и как обеспечить безопасность и предсказуемость этого процесса в реальном проекте. Удачной разработки!
-
Создано 31.12.2025 17:00:42
-
Михаил Русаков

Комментарии (0):
Для добавления комментариев надо войти в систему.
Если Вы ещё не зарегистрированы на сайте, то сначала зарегистрируйтесь.