Безопасность загружаемых файлов держится на четырех вещах: проверка типа по содержимому, а не по расширению, лимит размера, хранение вне web-root и virus scanning перед тем, как файл попадет в постоянное хранилище. Пропустишь один пункт, и форма загр…
400 000+ органических переходов за 3 месяца. Со-основатель GoBanana (231K пользователей, 12+ млн ₽ без рекламы) и NeuroScribe (65K пользователей). SEO/GEO-стратегии для AI-поисковиков, 1 700+ единиц контента, 17+ реализованных стратегий.
Об авторе →Claude Code: новый CLI-агент от Anthropic
Anthropic выпустила Claude Code — терминальный AI-агент для разработчиков. Инструмент работает прямо в командной строке и умеет писать, редактировать и запускать код.
Zcode AI: Полный гид по визуальному интерфейсу для Claude Code и AI-агентов
Узнайте, как использовать Zcode для управления Claude Code, Gemini и Codex в едином GUI. Настройка провайдеров, MCP-серверов и визуальный вайбкодинг.
YouTube-канал с монетизацией из любой точки мира: Пошаговый гайд 2026
Инструкция по созданию YouTube-канала: обход блокировок SMS, настройка расширенных функций через виртуальные номера и правила безопасности для монетизации.
Windsurf Code Maps: Как глубоко понимать архитектуру проекта перед написанием кода
Полный гайд по Windsurf Code Maps, модели Sway 1.5 и Sway Grep. Узнайте, как визуализировать архитектуру кода и ускорить разработку в 13 раз.
Vk Fast Cash Strategy
Аудитория ВКонтакте — это те же люди, что и в Instagram, но 'социальный контракт' площадки другой. Если Instagram — это 'дорогой ресторан' с демонстрацией успеха, то VK — это 'душевная шашлычная'. Здесь не работает глянцевый 'успешный успех
Безопасность загружаемых файлов держится на четырех вещах: проверка типа по содержимому, а не по расширению, лимит размера, хранение вне web-root и virus scanning перед тем, как файл попадет в постоянное хранилище. Пропустишь один пункт, и форма загрузки аватарки превращается в точку входа для веб-шелла. Ниже разберем каждый шаг с примерами на Node.js и Python, покажем реальные техники обхода валидации и дадим рабочий чеклист.
TL;DR: Расширение файла и заголовок Content-Type подделываются за секунду, поэтому единственный надежный способ определить тип файла — прочитать его magic bytes. К этому добавляются лимит размера, хранение файлов вне web-root и антивирусная проверка через ClamAV. В статье: рабочий код на Multer и FastAPI, разбор техник обхода фильтров и таблица инструментов для virus scanning.

Расширение легко переименовать, а Content-Type клиент отправляет сам, поэтому оба параметра не годятся для проверки безопасности загружаемых файлов.
По данным OWASP, доля веб-приложений хотя бы с одной уязвимостью загрузки файлов составляет 43%. Главная причина проста: проверка расширения через .endsWith('.jpg') ничего не говорит о реальном содержимом файла, а атакующий переименовывает shell.php в shell.php.jpg за один клик.

Расширение файла и Content-Type — метаданные, которые полностью контролирует клиент. В демонстрациях с CTF-платформ типа TryHackMe атакующие меняют MIME-тип на image/png прямо в перехваченном запросе через Burp Suite, и сервер, который проверяет только заголовок, пропускает PHP-скрипт как картинку. Второй частый трюк — двойное расширение: если фильтр ищет первое вхождение точки в имени файла, avatar.png.php пройдет проверку, а выполнится как PHP.
Отдельная история — magic bytes, они же file signature. Это несколько байтов в начале файла, которые однозначно определяют формат: PNG всегда начинается с 89 50 4E 47, а ZIP с 50 4B 03 04. Атакующий берет валидный PNG-заголовок, дописывает после него PHP-код и получает файл, который проходит проверку по сигнатуре, но исполняется как скрипт при определенных условиях сервера. Это называется polyglot-файл, и он обходит даже проверку по содержимому, если сервер не идет дальше первых байтов.

Надежная проверка типа читает первые байты файла и сверяет их со списком известных сигнатур, либо использует библиотеку вроде Pillow или python-magic, которая парсит структуру файла целиком.
Библиотека python-magic на Linux определяет тип файла по сигнатуре с точностью выше 95% для распространенных форматов. В связке с загрузкой изображений через Pillow сервер дополнительно проверяет, что файл открывается как валидное изображение, а не просто содержит нужные первые байты.
Node.js-разработчики решают эту задачу через file-type вместо доверия req.file.mimetype от Multer. Библиотека читает буфер файла и возвращает реальный MIME-тип по сигнатуре, а не по тому, что написал клиент в форме. Ниже рабочий пример на Express с Multer, где валидация идет в два слоя: fileFilter на входе плюс проверка магических байтов после сохранения в память.

const multer = require('multer');
const { fileTypeFromBuffer } = require('file-type');
const upload = multer({
storage: multer.memoryStorage(),
limits: { fileSize: 5 * 1024 * 1024 }, // 5 МБ
fileFilter: (req, file, cb) => {
const allowed = ['image/png', 'image/jpeg', 'image/webp'];
cb(null, allowed.includes(file.mimetype));
}
});
app.post('/upload', upload.single('avatar'), async (req, res) => {
const type = await fileTypeFromBuffer(req.file.buffer);
if (!type || !['png', 'jpg', 'webp'].includes(type.ext)) {
return res.status(400).json({ error: 'Файл не прошел проверку по содержимому' });
}
// дальше сохранение и virus scanning
});На Python с FastAPI та же логика реализуется через Pillow: сервер пытается открыть файл как изображение через Image.open() и перехватывает исключение, если файл поврежден или это не картинка вовсе. Это заодно закрывает часть атак через ImageMagick, потому что Pillow не выполняет чужой код при парсинге, в отличие от уязвимых обработчиков конвертации.
Лимит защищает не только диск от переполнения, но и сервер от DoS через отправку тысяч крупных запросов подряд, поэтому ограничивать нужно и размер файла, и общий размер запроса.
Пять мегабайт на аватарку и двадцать пять на документ, отсканированный с телефона, обычно хватает для 95% реальных сценариев любого SaaS-продукта. Занижать лимит агрессивно тоже не стоит: пользователи начнут жаловаться на ошибки и уходить с формы загрузки, не разобравшись в причине.
В Multer лимит задается прямо в конфиге через limits.fileSize, и превышение автоматически кидает MulterError с кодом LIMIT_FILE_SIZE, который нужно поймать и вернуть человеку понятный текст, а не пятисотую ошибку. В FastAPI такого встроенного лимита нет, поэтому размер проверяют вручную через Content-Length заголовок до чтения тела запроса, плюс дополнительно ограничивают буфер в конфигурации самого веб-сервера, например Nginx через client_max_body_size.
Отдельно стоит защитить сценарий с распаковкой архивов. Если приложение принимает ZIP и разворачивает его на сервере, лимит нужно проверять уже после декомпрессии, иначе миниатюрный архив в пару килобайт превращается в гигабайты данных на диске, это классическая атака zip bomb.

Файл, который лежит в публичной директории сайта, сервер может попытаться исполнить как скрипт, поэтому загрузки должны храниться отдельно от кода приложения и отдаваться через собственный обработчик.
Если директория uploads/ физически лежит внутри public/ или другой папки, которую веб-сервер отдает напрямую, любой файл, прошедший даже слабую валидацию, потенциально доступен по прямой ссылке и может быть исполнен. Именно так работают классические веб-шеллы: атакующий загружает shell.php, а потом просто открывает site.ru/uploads/shell.php в браузере.
Правильная схема: файлы хранятся вне web-root или вообще на отдельном сервере, объектном хранилище типа S3 или в managed-сервисе. Доступ к файлу происходит не по прямому пути, а через контроллер, который принимает someid и сам находит нужный файл на диске, отдавая его с правильными заголовками. Это заодно решает вторую проблему: имя файла от пользователя никогда не попадает напрямую в путь на файловой системе, что закрывает path traversal через ../../../etc/passwd в имени файла.

Имя сохраняемого файла лучше генерировать самим, через UUID, а не брать оригинальное от пользователя. Это одновременно убирает риск перезаписи существующих файлов чужим контентом и снимает вопрос с недопустимыми символами в имени.
ClamAV — бесплатный open-source антивирусный движок, который сканирует файл на сигнатуры известных угроз перед тем, как он попадет в постоянное хранилище приложения.
ClamAV в связке с Node.js подключается через библиотеку clamscan, которая обращается либо к локальному бинарнику, либо к демону clamd по TCP или unix-сокету. Актуальная стабильная ветка ClamAV на 2026 год это 1.4.x, и именно ее рекомендуют для продакшена как LTS-релиз.
const NodeClam = require('clamscan');
const clamscan = await new NodeClam().init({
clamdscan: { socket: '/run/clamav/clamd.sock' }
});
const { isInfected, viruses } = await clamscan.scanFile(filePath);
if (isInfected) {
await fs.unlink(filePath);
return res.status(422).json({ error: 'Файл не прошел проверку безопасности', viruses });
}По умолчанию ClamAV сканирует файлы размером до 25 МБ, поэтому для крупных документов лимит нужно либо расширять в конфиге, либо сканировать поток, а не готовый файл на диске, что снижает пиковую нагрузку на CPU при большом потоке загрузок. На стороне Python аналогичная связка идет через pyclamd, который работает поверх того же демона clamd и возвращает None, если файл чист, либо название угрозы, если сигнатура сработала.

Для команд, которые не хотят держать свой ClamAV-сервер, есть managed-варианты вроде VirusTotal API или MetaDefender: они дороже, но снимают заботу об обновлении баз сигнатур через freshclam, которое нужно делать регулярно, иначе антивирус со временем перестает ловить новые угрозы.
Оба стека закрывают одни и те же риски одинаковым набором приемов, разница в основном в библиотеках: Multer плюс file-type на Node.js против FastAPI, python-magic и Pillow на Python.
Выбор стека для загрузки файлов редко зависит от безопасности напрямую, она реализуется одинаково хорошо в обоих случаях. Разница в том, какие библиотеки уже есть в проекте и насколько команда привыкла к асинхронной обработке CPU-bound задач вроде обработки изображений.

| Задача | Node.js | Python |
|---|---|---|
| Прием multipart-формы | Multer | FastAPI + python-multipart |
| Проверка типа по содержимому | file-type | python-magic, Pillow |
| Лимит размера | multer limits.fileSize | ручная проверка Content-Length |
| Virus scanning | clamscan (ClamAV) | pyclamd (ClamAV) |
| CPU-bound обработка (ресайз) | worker_threads | run_in_threadpool (Starlette) |
В FastAPI отдельно стоит помнить про обработку CPU-bound задач вроде ресайза изображений: если делать это прямо в async-эндпоинте без run_in_threadpool из Starlette, event loop блокируется и все остальные запросы в этот момент простаивают. На Node.js аналогичная проблема решается через worker_threads, потому что обработка картинок средствами Sharp или Pillow — синхронная и тяжелая операция по своей природе.
Самые частые провалы: доверие Content-Type от клиента, проверка только по расширению без учета двойных расширений и хранение файлов в публичной директории с возможностью их исполнить.
Разбор реальных CTF-задач по теме file upload vulnerabilities показывает одну и ту же цепочку ошибок раз за разом. Сначала фильтр проверяет только расширение, потом в дело идет двойное расширение или альтернативные варианты вроде .php5, .phtml, которые сервер по недосмотру тоже исполняет как PHP. Дальше, если фильтр проверяет magic bytes, атакующий встраивает PHP-код прямо в чанки PNG-файла, оставляя корректный заголовок изображения.
| Техника обхода | Как работает | Что закрывает |
|---|---|---|
| Двойное расширение | shell.php.png при поиске первой точки | Проверка расширения по последней точке в имени |
| Подмена Content-Type | Клиент указывает image/png для скрипта | Проверка magic bytes на сервере |
| Polyglot-файл | Валидный заголовок PNG + встроенный код | Полный парсинг структуры файла (Pillow, python-magic) |
| Перезапись существующего файла | Совпадение имени с целевым файлом на сервере | Генерация имени файла через UUID |
| Path traversal в имени файла | ../../config.php в поле filename | Хранение вне web-root, санитизация имени |
Отдельная категория рисков возникает при обработке уже загруженного файла. Уязвимости в ImageMagick позволяли получить удаленное выполнение кода через специально собранное изображение, поэтому библиотеку для обработки картинок стоит держать актуальной версии и по возможности запускать обработку в изолированном контейнере или sandbox, а не в основном процессе приложения.
Максим: «Мог просто засесть до пяти ночи и просто там править одну какую-то функцию, которая не работала. Если бы не терпение, я бы это все закрыл. Но я понимал: это можно решить и нужно решить, чтобы идти дальше.»
Валидация загрузки файлов из той же категории задач. Кажется, что все проверки пишутся за час, но на деле каждый обходной путь всплывает только на третьей-четвертой итерации тестирования, и именно тут терпение решает больше, чем красивая архитектура кода.
Собрали все шаги в одном месте, чтобы свериться перед деплоем формы загрузки.

Для проектов, которые собираются на вайбкодинге через AI-инструменты, эта же логика ложится в промпт для генерации бэкенда: явно попросить модель не доверять расширению файла и добавить проверку magic bytes, иначе даже такой инструмент, как Claude Code, сгенерирует рабочий, но уязвимый эндпоинт загрузки по умолчанию, потому что это самый частый паттерн в обучающих данных.
89 50 4E 47 для PNG.../), позволяющая выйти за пределы разрешенной директории.Достаточно ли проверять расширение файла для базовой безопасности?
Нет, расширение переименовывается за секунду и не защищает вообще ни от чего серьезного. Минимальный рабочий уровень безопасности загружаемых файлов начинается с проверки magic bytes, лимита размера и хранения вне web-root.
Можно ли обойтись без virus scanning на старте проекта?
Для MVP с небольшим числом пользователей это допустимый компромисс, если загрузка ограничена узким списком типов вроде изображений и лимит размера строгий. При росте проекта и расширении списка принимаемых форматов ClamAV стоит подключать сразу.
Как проверить, что загружаемый файл действительно изображение?
Попробовать открыть его через Pillow в Python или Sharp в Node.js. Если библиотека выбрасывает ошибку, файл не является валидным изображением независимо от расширения или заголовка.
Что делать с загрузкой ZIP-архивов и других контейнерных форматов?
Проверять размер после распаковки, а не только исходного архива, и ограничивать глубину вложенности. Иначе миниатюрный zip bomb способен положить сервер.
Нужна ли аутентификация для эндпоинта загрузки файлов?
Да, практически всегда. Анонимная загрузка без ограничений на количество и частоту запросов быстро превращается в вектор для DoS и спама хранилища.
Как часто обновлять базы сигнатур ClamAV?
Через freshclam минимум раз в сутки, лучше через systemd-таймер или cron. Устаревшая база сигнатур пропускает свежие угрозы точно так же, как отсутствие антивируса вовсе.
Где хранить сами загруженные файлы в продакшене?
В объектном хранилище вроде S3 или аналогов, а не на локальном диске сервера приложения. Это снимает вопрос web-root вообще и упрощает масштабирование по мере роста числа файлов.
Если собираете бэкенд для загрузки файлов через AI-инструменты и хотите свериться, что валидация закрывает реальные риски, а не только базовый чек-лист, загляните в каталог AI-инструментов на VibeCoderz или запишитесь на консультацию к Максиму.