Безопасная загрузка изображений на сервер на PHP

Защитите сервер от хакеров! Разбираем уязвимости загрузки изображений и даем чек-лист безопасности, чтобы PHP-шеллы остались снаружи.

Загрузка файлов – одна из самых популярных функций в веб‑приложениях. К сожалению, она часто становится точкой входа для атак, когда злоумышленник пытается разместить на сервере исполняемый PHP‑скрипт под видом изображения. В этой статье мы разберём типичные уязвимости и покажем полный набор практических мер, которые позволяют свести риск к минимуму.


Содержание

  1. Почему загрузка изображений опасна?
  2. Типичные уязвимости
  3. Проверка на стороне PHP
  4. Настройки веб‑сервера (Apache / Nginx)
  5. Безопасное хранение и именование файлов
  6. Продвинутые техники защиты
  7. Тестирование и мониторинг

Почему загрузка изображений опасна?

Пример простейшего обхода (из реального кейса):

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

Ниже – пошаговый набор проверок, которые следует выполнить в скрипте, принимающем файл.

  1. Проверка наличия ошибки загрузки
  2. Ограничение размера файла
  3. Проверка MIME‑типа, полученного от $_FILES
  4. Проверка реального содержимого с помощью getimagesize или finfo_file
  5. Переименование файла (удаляем оригинальное расширение)
  6. Сохранение в директорию без исполнения 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. Чтобы избавиться от него, можно:

  1. Пересохранить изображение с помощью GD или Imagick:
    $src = imagecreatefromjpeg($dest);
     imagejpeg($src, $dest, 90); // Перезаписываем без метаданных
     imagedestroy($src);
     
  2. Для 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 % безопасность, но их совместное применение делает атаку практически невозможной.

Кратко повторим основные шаги:

  1. Проверять размер и ошибки загрузки.
  2. Определять реальный MIME‑тип через finfo и getimagesize;
  3. Генерировать случайные имена и сохранять файлы с правильным расширением.
  4. Хранить файлы в директории, где PHP‑код не исполняется (на уровне сервера).
  5. Пересохранять изображения, чтобы избавиться от скрытых метаданных.
  6. Дополнительно использовать CSP, rate‑limiting, антивирусный сканер и регулярное тестирование.

Следуя этим рекомендациям, вы существенно снизите риск компрометации вашего сайта через форму загрузки изображений.

Дата публикации: 00:12:03