Загрузка файлов – одна из самых популярных функций в веб‑приложениях. К сожалению, она часто становится точкой входа для атак, когда злоумышленник пытается разместить на сервере исполняемый PHP‑скрипт под видом изображения. В этой статье мы разберём типичные уязвимости и покажем полный набор практических мер, которые позволяют свести риск к минимуму.
Содержание
- Почему загрузка изображений опасна?
- Типичные уязвимости
- Проверка на стороне PHP
- Настройки веб‑сервера (Apache / Nginx)
- Безопасное хранение и именование файлов
- Продвинутые техники защиты
- Тестирование и мониторинг
Почему загрузка изображений опасна?
Пример простейшего обхода (из реального кейса):
POST /upload2.php HTTP/1.1
Content-Type: multipart/form-data; boundary=----xYzZY
------xYzZY
Content-Disposition: form-data; name="userfile"; filename="shell.php"
Content-Type: text/plain
<?php system($_GET['cmd']); ?>
------xYzZY--
Если сервер проверяет только MIME‑тип, то такой запрос может пройти, а файл будет выполнен как PHP‑скрипт.
Типичные уязвимости
- Проверка только MIME‑типа – клиент может подменить .
- Проверка только расширения –
picture.jpg.phpбудет воспринят как изображение. - Отсутствие ограничения размеров – загрузка огромных файлов может привести к отказу в обслуживании.
- Сохранение в директорию, где разрешено исполнение PHP – любой загруженный скрипт будет выполнен.
- Отсутствие проверки реального содержимого – файлы‑псевдо‑изображения (например,
GIF89a+ PHP‑код) могут пройти проверку.
Проверка на стороне PHP
Ниже – пошаговый набор проверок, которые следует выполнить в скрипте, принимающем файл.
- Проверка наличия ошибки загрузки
- Ограничение размера файла
- Проверка MIME‑типа, полученного от
$_FILES - Проверка реального содержимого с помощью
getimagesizeилиfinfo_file - Переименование файла (удаляем оригинальное расширение)
- Сохранение в директорию без исполнения PHP
Полный пример кода
<?php
// 1. Конфигурация
define('MAX_FILE_SIZE', 2 * 1024 * 1024); // 2 МБ
$allowedMime = ['image/jpeg', 'image/png', 'image/gif'];
$uploadDir = __DIR__ . '/uploads/'; // Директория без исполнения PHP
// 2. Проверка, что файл действительно загружен
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
die('Ошибка загрузки файла.');
}
// 3. Ограничение размера
if ($_FILES['image']['size'] > MAX_FILE_SIZE) {
die('Файл слишком большой.');
}
// 4. Проверка MIME‑типа, пришедшего от браузера
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($_FILES['image']['tmp_name']);
if (!in_array($mime, $allowedMime, true)) {
die('Недопустимый тип файла.');
}
// 5. Дополнительная проверка реального содержимого
$imgInfo = getimagesize($_FILES['image']['tmp_name']);
if ($imgInfo === false) {
die('Файл не является изображением.');
}
if (!in_array($imgInfo['mime'], $allowedMime, true)) {
die('Несоответствие MIME‑типа изображения.');
}
// 6. Генерация безопасного имени
$ext = image_type_to_extension($imgInfo[2]); // .jpg, .png, .gif
$hash = bin2hex(random_bytes(16));
$dest = $uploadDir . $hash . $ext;
// 7. Перемещение файла
if (!move_uploaded_file($_FILES['image']['tmp_name'], $dest)) {
die('Не удалось сохранить файл.');
}
// 8. Установка прав (чтение только)
chmod($dest, 0644);
echo 'Файл успешно загружен.';
?>
Ключевые моменты:
- Используем
finfo_file– проверка реального MIME‑типа, а не заголовка. getimagesizeгарантирует, что файл действительно является изображением.- Расширение берётся из функции
image_type_to_extension, а не из имени оригинального файла. - Имя файла генерируется случайным хешем – исключаем коллизии и предсказуемость.
Настройки веб‑сервера
Даже при идеальном PHP‑коде рекомендуется «запереть» директорию загрузок на уровне сервера.
Apache (.htaccess)
# Запретить исполнение любого кода в папке uploads Order allow,deny
Deny from all
</FilesMatch>
AllowOverride None
RemoveType .php .phtml .php5
Nginx
# Пример location для папки uploads
location /uploads/ {
# Запретить исполнение скриптов deny all;
}
# Отключить обработку index.php и т.п.
try_files $uri =404;
}
Эти правила гарантируют, что даже если вредоносный PHP‑файл всё‑таки окажется в директории, веб‑сервер не будет его интерпретировать.
Безопасное хранение и именование файлов
- Хранить вне корня сайта (например,
/var/www/uploads/и отдавать через скрипт). - Не использовать оригинальное имя файла – оно может содержать опасные символы.
- Для доступа к изображению использовать скрипт‑прокси:
<img src="show_image.php?file=abcdef123456.jpg" alt="...">где
show_image.phpпроверяет, что запрошенный файл действительно находится в папке загрузок и отдает его с заголовкомContent-Type: image/*.
Продвинутые техники защиты
Проверка «скрытого» кода в изображениях
Некоторые атакующие встраивают PHP‑код в метаданные JPEG/PNG. Чтобы избавиться от него, можно:
- Пересохранить изображение с помощью GD или Imagick:
$src = imagecreatefromjpeg($dest); imagejpeg($src, $dest, 90); // Перезаписываем без метаданных imagedestroy($src); - Для PNG:
$src = imagecreatefrompng($dest); imagepng($src, $dest); imagedestroy($src);
Ограничение количества запросов (rate‑limiting)
Брутфорс‑атаки на загрузку можно замедлить с помощью mod_evasive (Apache) или limit_req_zone (Nginx).
Использование Content‑Security‑Policy (CSP)
Запретить исполнение скриптов из папки загрузок в браузере:
Content-Security-Policy: script-src 'self'; object-src 'none'; Сканирование антивирусом
После сохранения файла можно запускать clamscan или malware scanner для дополнительного уровня защиты.
Тестирование и мониторинг
Регулярно проверяйте свою реализацию:
- Попробуйте загрузить
shell.phpс заголовкомimage/jpeg– должно быть отклонено. - Загрузите
picture.jpg.php– сервер не должен его исполнять. - Проверьте, что файлы сохраняются с правами
0644и находятся в директории без PHP‑обработки. - Настройте логирование
$_FILESи ошибок PHP, а также мониторинг доступа к директории/uploads/.
Для автоматизации можно использовать OWASP ZAP или Burp Suite с модулем «File Upload». Они позволяют генерировать запросы с подменой MIME‑типа, расширения и содержимого.
Защита от загрузки вредоносных PHP‑файлов – это многоуровневый процесс. Ни одна отдельная мера (только проверка MIME, только .htaccess и т.д.) не гарантирует 100 % безопасность, но их совместное применение делает атаку практически невозможной.
Кратко повторим основные шаги:
- Проверять размер и ошибки загрузки.
- Определять реальный MIME‑тип через
finfoиgetimagesize; - Генерировать случайные имена и сохранять файлы с правильным расширением.
- Хранить файлы в директории, где PHP‑код не исполняется (на уровне сервера).
- Пересохранять изображения, чтобы избавиться от скрытых метаданных.
- Дополнительно использовать CSP, rate‑limiting, антивирусный сканер и регулярное тестирование.
Следуя этим рекомендациям, вы существенно снизите риск компрометации вашего сайта через форму загрузки изображений.
Дата публикации: 00:12:03