Защита модулей загрузки файлов от атак клиентской подделки
Файловые загрузки остаются одной из самых уязвимых частей веб-приложений. Даже «небольшие» ошибки в обработке загруженных файлов часто приводят к удалённому выполнению кода на сервере или краху безопасности проекта. Разработчикам нужно понимать распространённые ошибки и возможные сценарии атак, чтобы строить надёжные защитные механизмы.
Что такое клиентская подделка?
Клиентская подделка — это фундаментальная идея в веб-безопасности: нельзя доверять любым данным, которые приходят от клиента. Под клиентской подделкой подразумевается любое намеренное изменение данных на стороне клиента до отправки на сервер. В контексте загрузки файлов это прежде всего:
- имя загруженного файла;
- значение заголовка Content-Type (MIME-типа), отправляемое браузером или клиентом.
Оба этих значения полностью управляются клиентом и могут быть подделаны. Имя файла может содержать любые символы и несколько точек; Content-Type легко изменить в запросе. Это даёт злоумышленнику пространство для обхода примитивных проверок на стороне сервера.
Важно: серверные проверки должны базироваться на фактическом содержимом файла, а не только на метаданных, присланных клиентом.
Расширение файла и белые списки
Первый технический барьер при обработке загрузок — белый список допустимых расширений. Но простая проверка расширения по имени файла ненадёжна. Проблемные сценарии:
- имя «muo.jpeg.php» обходит простые проверки по последней точке;
- регистр расширения может отличаться (.JPEG, .Jpg);
- расширение отсутствует вовсе.
Пример простого PHP-кода из исходного текста (сохранён):
$file_parts = pathinfo($filename);
switch($file_parts['extension'])
{
case "jpg":
break;
case "bat": // Or exe, dll, so, etc.
break;
case "":
case NULL: // No file extension
break;
} Этот фрагмент демонстрирует идею, но требует доработки. Рекомендации:
- Приводите расширение к нижнему регистру и сравнивайте со списком разрешённых расширений.
- Не доверяйте только имени — проверяйте содержимое файла (finfo или аналог).
- Генерируйте безопасные имена файлов на сервере (например, UUID или хэш).
- Скрывайте структуру и расположение директории загрузки.
Пример более надёжной логики на PHP (иллюстрация):
// Пример проверки расширения и содержимого
$allowed_ext = ['jpg','jpeg','png','gif'];
$original_name = $_FILES['file']['name'];
$ext = strtolower(pathinfo($original_name, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_ext)) {
throw new Exception('Недопустимое расширение');
}
// Проверяем MIME по содержимому
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($mime, ['image/jpeg','image/png','image/gif'])) {
throw new Exception('Недопустимый MIME-тип');
}
// Безопасное имя
$safe_name = bin2hex(random_bytes(16)) . '.' . $ext;
move_uploaded_file($_FILES['file']['tmp_name'], '/var/www/uploads/' . $safe_name);Дополнительно: при работе с изображениями полезно принудительно пересоздавать изображение на сервере (например, с помощью GD или ImageMagick). Это исключает возможность загрузки полиглот-файла, который одновременно является изображением и исполняемым кодом.
Что такое информация Content-Type?
Content-Type — это MIME-тип, который браузер указывает при отправке файла в HTTP-запросе. Этот заголовок помогает серверу понять, с каким типом содержимого он имеет дело. Однако значение Content-Type приходит от клиента и может быть подделано.
Почему Content-Type нельзя доверять:
- злоумышленник может изменить заголовок или сформировать сырой запрос с ложным MIME;
- некоторые клиенты или прокси выполняют «MIME-sniffing» и могут интерпретировать контент по-своему.
Что нужно делать:
- Всегда проверяйте MIME по фактическому содержимому файла (finfo, getimagesize для изображений).
- Устанавливайте заголовок X-Content-Type-Options: nosniff на стороне сервера, чтобы браузеры не «угадывали» MIME и не интерпретировали контент иначе.
- При возможности пересоздавайте/пересохраняйте файлы на сервере (ревалидация), чтобы удалить скрытые данные и полезные нагрузки.
Пример проверки MIME в PHP (фрагмент выше) и вызов getimagesize для изображений:
$info = @getimagesize($_FILES['file']['tmp_name']);
if ($info === false) {
throw new Exception('Файл не является изображением');
}
// $info[2] содержит константу IMAGETYPE_..., можно дополнительно проверить соответствие расширениюТакже важно логировать несоответствия MIME и расширения для последующего анализа инцидентов.
SWF (Flash) и последовательность атаки
Поддержка Adobe Flash официально завершена, но на некоторых системах и браузерах всё ещё могут встречаться плагины или встроенные реализации. Flash/SWF-файлы могут быть исполнены браузером с типом application/x-shockwave-flash, даже если расширение файла выглядит как .jpeg, если браузер или плагин принудительно интерпретирует файл как SWF.
Типичный сценарий атаки:
- Злоумышленник загружает SWF-файл с именем image.jpeg. Сервер пропускает файл по расширению, а Content-Type был подделан клиентом.
- Файл оказывается по URL: https://www.target-site.com/images/image.jpeg.
- На сайте attacker.com злоумышленник встраивает этот URL в
- Посетитель attacker.com получает страницу, браузер загружает SWF с сайта target-site и выполняет его. Если SWF управляет запросами, он может отправлять авторизованные запросы к target-site от имени залогиненного пользователя, обходя ограничения (например, CSRF).
Оригинальный фрагмент HTML из сценария атаки (сохранён):
style="height:1px;width:1px;" data="www.target-site.com/images/image.jpeg" type="application/x-shockwave-flash" allowscriptaccess="always" flashvars="c=read&u=somethings" Меры противодействия:
- Храните загруженные файлы на отдельном поддомене, например: https://file.target-site.com/images/image.jpeg. Отдельный поддомен можно настроить так, чтобы он не отправлял пользовательские cookie для основного домена (set-cookie domain) и имел отдельную политику CORS.
- Отдавайте файлы с заголовком Content-Disposition: attachment; filename=”…” — тогда браузер предложит скачать файл, а не исполнит его встроенно.
- Устанавливайте X-Content-Type-Options: nosniff, чтобы запретить «угадать» MIME.
- Отключите поддержку плагинов и параметров, таких как allowscriptaccess, на страницах, где не нужен сторонний функционал.
- По возможности полностью запретите хранение исполняемых форматов (SWF, HTML, PHP, EXE) в директориях публичной отдачи.
Практические рекомендации — чеклист и SOP для команд
Ниже приведён чеклист ролей и минимальный план действий для внедрения безопасной обработки файлов.
Чеклист для разработчика
- Использовать белый список расширений и сравнивать в нижнем регистре.
- Проверять MIME по содержимому (finfo, getimagesize).
- Генерировать безопасные серверные имена файлов.
- Пересоздавать изображения на сервере (ревалидация/ребилд).
- Ограничивать максимальный размер загружаемых файлов.
- Устанавливать строгие права доступа на каталоги загрузки (chmod 0700/0750).
- Логировать аномалии и блокированные попытки.
Чеклист для DevOps/инфраструктуры
- Отдавать статические файлы с отдельного поддомена или через объектное хранилище (S3, GCS) без cookie основного сайта.
- Добавлять заголовки безопасности: X-Content-Type-Options: nosniff, Content-Security-Policy, Strict-Transport-Security.
- Конфигурировать веб-сервер так, чтобы файлы в uploads не могли исполняться (deny exec, не подключать PHP в каталоге загрузок).
- Настроить сканирование загруженных файлов антивирусом/антивредоносом.
Чеклист для QA/безопасности
- Писать тесты на обходы белых списков и проверять поведение при двойных расширениях.
- Проводить fuzz-тестирование загрузок.
- Проверять реакции сервера на полиглот-файлы и файлы с некорректным MIME.
SOP при обнаружении уязвимости загрузки файла
- Немедленно вынести каталог загрузок из публичной зоны (сделать файлы недоступными по URL).
- Проанализировать логи и найти все подозрительные загрузки.
- Удалить/изолировать подозрительные файлы.
- Закрыть уязвимый код/патч и запустить регрессионные тесты.
- Провести ретроспективу и обновить чеклисты.
Критерии приёмки
- Все загружаемые файлы проходят проверку расширения и содержимого.
- Файлы хранятся под безопасными именами и с правами 0600/0640.
- Отдача файлов не осуществляется с основного домена и не использует cookie.
- Логи фиксируют любые несоответствия и превышения лимитов.
Когда базовые механизмы не сработают — сценарии обхода
- Полиглот-файлы: файл может содержать валидные данные для двух форматов одновременно (напр., изображение + встраиваемый скрипт).
- Серверная ошибка конфигурации: если PHP выполняется в директории uploads, даже безопасно называемый файл может исполниться.
- Недостаточная валидация на стороне клиента и отсутствие контроля на сервере.
- Старые браузеры/плагины могут интерпретировать контент иначе и выполнять его.
Контрмера: всегда применяйте мультиуровневую защиту — белый список, проверка содержимого, пересоздание/очистка, заголовки безопасности и изоляция домена.
Альтернативные подходы и дополнительные меры
- Хранение в объектном хранилище (S3/GCS) с настроенными политиками доступа и «безопасной» отдачей через CDN.
- Пересоздание изображения: загрузка → серверный ресайз/кодирование → сохранение нового файла, что устраняет вредоносные вставки.
- Антивирусное сканирование загружаемых файлов.
- Ограничение диапазона допустимого контента: если нужен только аватар, разрешать только небольшие размером файлы с жёстким ресайзом.
- Замена исполняемых форматов на безопасные представления (например, превью вместо оригинала).
Примеры конфигураций и сниппеты
Nginx: разрешаем отдачу, запрещаем исполнение и добавляем заголовки безопасности
server {
listen 443 ssl;
server_name file.target-site.com;
root /var/www/uploads;
location / {
# Не обрабатываем PHP в этом каталоге
location ~* \.(php|phtml)$ {
deny all;
}
add_header X-Content-Type-Options nosniff;
add_header Content-Security-Policy "default-src 'none';";
add_header X-Frame-Options DENY;
}
}HTTP-ответ: принудительное скачивание
HTTP/1.1 200 OK
Content-Disposition: attachment; filename="image.jpeg"
Content-Type: application/octet-streamПримечание: Content-Disposition: attachment уменьшит шанс, что браузер исполнит файл встроенно.
Тесты и критерии приёмки для автоматизации
Тестовые кейсы:
- Загрузка файла image.jpeg с реальным JPEG-содержимым — должен пройти.
- Загрузка файла image.jpeg, но содержащего SWF/EXE — должен быть отклонён/помечен.
- Попытка загрузки файла с двойным расширением muo.jpeg.php — отклонена.
- Попытка доступа к загруженному файлу не должна отдавать cookie домена основного сайта.
- Проверка заголовков ответа: X-Content-Type-Options присутствует; Content-Disposition — attachment для неподтверждённых форматов.
Критерии приёмки (повтор): все тесты должны пройти в CI и регрессия не должна возвращать уязвимости загрузки.
Уровни зрелости защиты загрузок
- Уровень 1 (базовый): проверка расширения по имени и ограничение размера.
- Уровень 2 (средний): проверка MIME по содержимому, логирование, безопасное имя.
- Уровень 3 (высокий): пересоздание файлов, антивирус, отдельный поддомен, строгие заголовки.
- Уровень 4 (превосходный): автоматическое сканирование на полиглот-структуры, honeypot-файлы, блокировка по репутации IP и интеграция в процесс инцидент-менеджмента.
Конфиденциальность и соответствие требованиям
Если загружаются персональные данные, убедитесь, что вы соблюдаете требования конфиденциальности и хранения данных (например, GDPR). Основные правила:
- Уведомляйте пользователя об использовании хранилища и целях обработки.
- Ограничивайте доступ к файлам по принципу наименьших привилегий.
- Архивируйте и удаляйте файлы в соответствии с политикой хранения данных.
Краткая сводка и выводы
- Никогда не доверяйте имени файла и заголовку Content-Type, присланным клиентом.
- Всегда проверяйте содержимое файла на сервере (finfo, getimagesize), используйте белые списки и пересоздание файлов.
- Изолируйте хранилище файлов: отдельный поддомен или объектное хранилище, заголовки безопасности и Content-Disposition: attachment.
- Внедрите роли и процессы: разработчики, DevOps и безопасность должны иметь чёткие обязанности и тесты.
Важное. Защита загрузок — это многоуровневая задача. Комбинация контроля содержимого, конфигурации сервера и процессов реагирования значительно снижает риск успешной эксплуатации уязвимостей.
Сводка — что делать в первую очередь:
- Добавьте проверку содержимого (finfo/getimagesize).
- Используйте белые списки расширений и безопасные имена.
- Храните файлы на отдельном поддомене и добавляйте X-Content-Type-Options: nosniff.
- Пересоздавайте/обрабатывайте файлы перед публикацией и сканируйте на вредоносный код.
Конец статьи.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone