CVE-2026 Node.js multipart parser — уязвимости multer и чек-лист AppSec для код-ревью

Девять CVE за шесть месяцев 2026 года. В одном npm-пакете. Multer — стандартное Express.js-middleware для обработки multipart/form-data (это формат, в котором браузер отправляет формы с файлами). Четыре из девяти получили CVSS 7.5 (HIGH), три — 8.7 по шкале CVSS 4.0, одна — 5.3 (MEDIUM), одна — 3.7 (LOW). Восемь из девяти эксплуатируются одним HTTP-запросом без аутентификации и без участия пользователя (исключение — CVE-2026-77063, где нужен точный тайминг race condition при AC:H, и импакт ограничен). При этом multer сидит в зависимостях практически любого Express-приложения, которое принимает загрузку файлов.
Если ты только выстраиваешь практику AppSec код-ревью — это не абстрактный кейс из advisory. Это девять конкретных паттернов, которые нужно уметь находить в коде до того, как они станут инцидентом.
Как multipart/form-data обрабатывается в Node.js
Чтобы понять, на каком уровне ломается парсер, нужно представлять цепочку обработки данных от браузера до твоего кода.
Когда пользователь отправляет форму с файлом, тело HTTP-запроса кодируется в формат multipart/form-data — набор «частей» (parts), разделённых boundary-строкой (уникальный разделитель, указанный в заголовке Content-Type). Каждая часть содержит заголовки (имя поля, имя файла, тип содержимого) и тело (содержимое файла или значение текстового поля).
В Express-приложении обработку берёт на себя middleware — чаще всего multer. Под капотом multer использует библиотеку busboy для потокового разбора байтов по мере поступления из сети. Значения текстовых полей передаются в зависимость append-field, которая записывает их в JavaScript-объект req.body, интерпретируя bracket notation в именах полей (например, items[0] → массив, user[name] → вложенный объект).
Ключевой момент: вся цепочка (multer → busboy → append-field) отрабатывает ДО твоего обработчика маршрута. Middleware аутентификации, валидация в контроллере, rate limiter на уровне приложения — всё включается позже. Уязвимость парсера срабатывает раньше любой бизнес-логики.
По классификации MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1190 — её идентификаторы) эксплуатация таких уязвимостей соответствует технике T1190 — Exploit Public-Facing Application (тактика Initial Access). Результат DoS-атаки — T1499.004, Application or System Exploitation (тактика Impact). По OWASP API Security Top 10 это API4:2023 — Unrestricted Resource Consumption.
Бизнес-логика атаки проста: один HTTP-запрос размером в несколько килобайт роняет Node.js-процесс или блокирует event loop (единственный поток, который обрабатывает все запросы в Node.js). Для SaaS-платформы с загрузкой файлов — дешёвый способ положить сервис.
Три класса эксплуатации CVE Node.js multipart-парсеров
Девять CVE в multer группируются в три класса по механизму. Это не академическая классификация ради красоты — каждый класс требует отдельного подхода при проверке кода.
Ресурсные DoS через имена полей
Четыре CVE эксплуатируют одну и ту же зависимость — append-field, которая разбирает bracket notation в именах полей формы. Зависимость, о существовании которой большинство разработчиков даже не подозревает.
CVE-2026-77078 (CVSS 7.5, CWE-248 — Uncaught Exception). Два текстовых поля в запросе: первое с именем a[4294967294] — огромный числовой индекс, который заставляет append-field создать sparse array (разреженный массив — массив, где выделена память под максимальную длину, но заполнены единичные ячейки) максимальной длины. Второе с именем a[] пытается добавить элемент через push — длина массива превышает допустимый максимум для JavaScript, V8 бросает RangeError: Invalid array length. Ошибка не перехватывается — процесс падает.
CVE-2026-82333 (CVSS 7.5, CWE-400 — Uncontrolled Resource Consumption). Тот же механизм со sparse array, но второе поле имеет нечисловой ключ, что заставляет append-field синхронно итерировать весь массив. Event loop блокируется — сервер перестаёт отвечать всем клиентам.
CVE-2026-5079 (CVSS 7.5, CWE-400). Глубоко вложенные имена полей через bracket notation: a[b][c][d]...[z] без ограничения глубины. Парсер рекурсивно создаёт вложенные объекты, потребляя CPU и память.
CVE-2026-3304 (CVSS 8.7 по CVSS 4.0, CWE-459). Малформированные запросы вызывают исчерпание ресурсов — без workaround, обновление до multer 2.1.0 обязательно. В отличие от трёх предыдущих CVE, NVD описывает механизм обобщённо (‘malformed requests’) без указания на bracket-notation/append-field — точный вектор атаки не раскрыт.
Утечки ресурсов при обрыве загрузки
Три CVE возникают, когда клиент обрывает соединение посреди загрузки файла. Сценарий банальный: начал отправлять файл — оборвал. Повторил сотню раз.
CVE-2026-77037 (CVSS 7.5, CWE-400 + CWE-459 — Incomplete Cleanup). В multer 2.2.0 при обрыве загрузки disk storage engine удаляет видимый файл, но не закрывает файловый дескриптор (file descriptor — ресурс ОС для работы с открытым файлом; их количество ограничено). Дескриптор остаётся «висеть» до перезапуска процесса. Повторные обрывы исчерпывают лимит дескрипторов ОС — сервер теряет возможность открывать файлы и сокеты.
CVE-2026-5038 (CVSS 5.3, CWE-459). Версии 2.0.0-alpha.1 — 2.1.1: Readable.pipe() не пробрасывает сигнал destroy на fs.WriteStream, недописанные файлы остаются на диске. Атакующий забивает дисковое пространство повторными обрывами.
CVE-2026-2359 (CVSS 8.7 по CVSS 4.0, CWE-772 — Missing Release of Resource). Обрыв соединения во время загрузки вызывает утечку ресурсов. Единственное решение — обновление до multer 2.1.0, workaround отсутствует.
Race condition и переполнение стека
CVE-2026-77063 (CVSS 3.7, CWE-362 — Race Condition). При одновременном использовании асинхронного fileFilter и лимита fileSize race condition позволяет файлу, превышающему лимит, пройти проверку. Реальный импакт ограничен: busboy обрезает поток на лимите, так что это обход валидации, а не бесконтрольное потребление.
CVE-2026-3520 (CVSS 8.7 по CVSS 4.0, CWE-674 — Uncontrolled Recursion + CWE-770). Малформированные запросы вызывают переполнение стека — процесс падает.
Сводная таблица уязвимостей multer 2026
| CVE | CVSS | Класс | Механизм | Исправлено в |
|---|---|---|---|---|
| CVE-2026-77078 | 7.5 HIGH | Ресурсный DoS | Sparse array, RangeError, crash | 2.3.0 |
| CVE-2026-82333 | 7.5 HIGH | Ресурсный DoS | Sparse array, event loop block | 2.3.0 |
| CVE-2026-5079 | 7.5 HIGH | Ресурсный DoS | Deep nesting bracket notation | 2.2.0 |
| CVE-2026-3304 | 8.7 HIGH (v4) | Ресурсный DoS | Malformed request, resource exhaustion | 2.1.0 |
| CVE-2026-77037 | 7.5 HIGH | Утечка ресурсов | File descriptor leak on abort | 2.3.0 |
| CVE-2026-5038 | 5.3 MEDIUM | Утечка ресурсов | Orphaned files on disk | 2.2.0 |
| CVE-2026-2359 | 8.7 HIGH (v4) | Утечка ресурсов | Connection drop, resource exhaustion | 2.1.0 |
| CVE-2026-3520 | 8.7 HIGH (v4) | Stack overflow | Malformed request, stack overflow | 2.1.1 |
| CVE-2026-77063 | 3.7 LOW | Race condition | Async fileFilter + fileSize bypass | 2.3.0 |
По данным CISA SSVC (Stakeholder-Specific Vulnerability Categorization — методика приоритизации реагирования на уязвимости), CVE с доступными CISA Vulnrichment-данными (CVE-2026-77037, CVE-2026-77078, CVE-2026-5079, CVE-2026-5038, CVE-2026-2359) имеют статус exploitation: none (не эксплуатируются in the wild) и automatable: yes (легко автоматизировать). Для CVE-2026-3304 и CVE-2026-3520 CISA SSVC enrichment на момент публикации отсутствует. EPSS (Exploit Prediction Scoring System — вероятность от 0 до 1, что CVE начнут эксплуатировать в ближайшие 30 дней) для ранних CVE чуть выше медианы: CVE-2026-2359 — 0.0066 (перцентиль 0.50), CVE-2026-3304 — 0.0066 (перцентиль 0.50), CVE-2026-3520 — 0.0071 (перцентиль 0.52). Минимальный сигнал, а не выраженный тренд.
Разбор CVE-2026-77078 — как два поля роняют Node.js-процесс
Чтобы понять, что именно искать при проверке кода, разберём CVE-2026-77078 до уровня конкретных строк. Самый наглядный пример из девяти: один запрос, два поля, процесс лежит.
Зависимость append-field записывает значения полей формы в объект req.body. Когда multer получает текстовое поле с именем items[4294967294], append-field интерпретирует число в скобках как индекс массива:
// append-field: упрощённая логика (до патча)
// path = ["items", "4294967294"], value = "x"
obj["items"] = obj["items"] || [];
obj["items"][4294967294] = value;
// V8 создаёт sparse array максимальной длины (2^32-1)
// Второе поле items[] вызывает push()
// push() пытается установить индекс 2^32-1 → длина > 2^32-1 → RangeError
Массив выделяется. Приходит второе поле items[] — append-field вызывает Array.push(), который пытается установить элемент с индексом, превышающим 2^32 - 1 (максимум для JavaScript-массива). V8 бросает RangeError, multer его не ловит — uncaught exception, process.exit.
С точки зрения атакующего: POST-запрос размером ~200 байт роняет процесс. pm2 или systemd перезапускает — атакующий повторяет. Цена атаки — ноль. Для mitigation в multer 2.3.0 добавлена опция limits.fieldArrayIndexLimit, которая отклоняет индексы больше заданного значения.
AppSec код-ревью Node.js — что проверять до патча CVE
Практический чек-лист. Применим и до обновления (легаси, заморозка релизов), и после — как защита в глубину.
Шаг 1: определить версию multer. В корне проекта — npm ls multer. Команда покажет установленную версию и откуда она попала в дерево зависимостей. Если multer приходит транзитивно (через другой пакет), обновление package.json напрямую не поможет — нужно обновлять родительскую зависимость или использовать overrides.
Шаг 2: найти эндпоинты без лимитов. Ищи в коде вызовы multer() или multer({}) без явного объекта limits. До multer 2.3.0 дефолтные лимиты не ограничивали ни глубину вложенности полей, ни числовые индексы массивов, ни количество полей. Нет limits = уязвимый паттерн вне зависимости от версии, потому что следующий CVE может эксплуатировать тот же пробел.
Шаг 3: проверить аутентификацию на upload-маршрутах. Все девять CVE имеют CVSS-вектор PR:N (привилегии не нужны). Если POST /upload открыт анонимам — это приоритет номер один.
Шаг 4: проверить тип хранилища. CVE-2026-77037 и CVE-2026-5038 касаются только diskStorage. Если приложение использует memoryStorage, утечки дескрипторов и файлов не применимы (но ресурсные DoS через имена полей работают с любым storage).
Semgrep-правило для SAST Node.js приложений
Semgrep (open-source SAST-инструмент для поиска паттернов в коде) позволяет написать кастомное правило, которое находит вызовы multer() без явного limits:
# multer-no-limits.yaml — пример правила
rules:
- id: multer-missing-limits
pattern-either:
- pattern: multer()
- patterns:
- pattern: multer($OPTS)
- pattern-not: multer({..., limits: $LIMITS, ...})
message: >
multer() без limits — уязвим к CVE-2026-77078/82333/5079
severity: WARNING
languages: [javascript, typescript]
Запуск: semgrep --config multer-no-limits.yaml ./src (нужен установленный semgrep — ставится через pip install semgrep). Если правило срабатывает — ты нашёл потенциально уязвимый эндпоинт. На выходе: путь к файлу, номер строки и сообщение из поля message. Дальше — проверить версию multer и добавить явные лимиты.
Митигация CVE Node.js multipart — практические шаги
Приоритет первый — обновление multer. Multer 2.3.0 закрывает все девять CVE. После обновления проверь результат: npm ls multer должен показать multer@2.3.0 или выше. Если multer приходит транзитивно, добавь секцию overrides в package.json: "overrides": { "multer": ">=2.3.0" }.
Дополнительно установи limits.fieldArrayIndexLimit и limits.fieldNestingDepth — они защищают от CVE-2026-77078, CVE-2026-82333 и CVE-2026-5079. Значения зависят от приложения: если формы не используют массивы — ставь fieldArrayIndexLimit: 100, если нет вложенных объектов — fieldNestingDepth: 3.
Приоритет второй — ограничения на уровне инфраструктуры. Если немедленное обновление невозможно:
— В nginx: client_max_body_size 10m; ограничивает общий размер запроса. Не защищает от CVE-2026-77078 (крошечный запрос роняет процесс), но снижает импакт утечек дескрипторов.
— Rate limiting на upload-маршрутах: limit_req zone=uploads burst=5 замедляет эксплуатацию, но не блокирует полностью.
— WAF-правила на количество полей в multipart-запросе — если ваш WAF умеет инспектировать multipart-тело.
Приоритет третий — мониторинг. Настрой алерты на рост p99 latency на upload-эндпоинтах, увеличение числа перезапусков процесса (exit code 1 — uncaught exception) и рост количества открытых файловых дескрипторов (lsof -p <pid> | wc -l).
Уязвимость multipart parser за пределами multer
Проблема не в одном пакете. В мае 2026 раскрыта CVE-2026-42561 в python-multipart — стриминговом парсере для Python-фреймворков Starlette и FastAPI. Механизм тот же: MultipartParser не ограничивал ни количество заголовков части, ни размер каждого заголовка (CWE-770 — Allocation of Resources Without Limits + CWE-606). CVSS 7.5 (HIGH), вектор AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H — атака по сети, без привилегий, без действий пользователя, импакт на доступность. Исправлено в python-multipart 0.0.27, подробности — в GitHub Security Advisory GHSA-pp6c-gr5w-3c5g. Red Hat классифицировал уязвимость как Important и выпустил патчи для Ansible Automation Platform 2.6 и 2.7 (RHSA-2026:42132, RHSA-2026:42142).
Отдельный класс — CVE-2026-58044 в самом Node.js: HTTP request smuggling (CWE-444 — Inconsistent Interpretation of HTTP Requests). HTTP-парсер Node.js скрывает заголовки, превышающие maxHeadersCount, от req.headers и req.rawHeaders, но продолжает использовать их для фрейминга. Content-Length, «спрятанный» за лимитом, невидим для кода приложения, хотя парсер всё равно читает тело. Для forwarding proxy на Node.js это рассинхронизация — предусловие для HTTP request smuggling. Red Hat выпустил патчи в рамках Hardened Images (RHSA-2026:48305, RHSA-2026:48537).
Ещё один сигнал — CVE-2026-87776 в npm-пакете compression (Express-middleware для gzip-сжатия): при обрыве соединения zlib-стрим не уничтожается, утекает нативная память (CWE-401 + CWE-459). CVSS 7.5, исправлено в compression 1.8.2. Паттерн тот же: ресурс не освобождается при аварийном завершении операции.
Стриминговые парсеры разных экосистем совершают одни и те же ошибки — не ставят верхние границы на размер и количество обрабатываемых структур. По OWASP это A06:2021 — Vulnerable and Outdated Components: проблема не в твоём коде, а в зависимостях.
Девять CVE за полгода в одном пакете — и четыре из них не в multer напрямую, а в append-field, зависимости, о которой большинство разработчиков не слышали. Это системная проблема npm, где критический путь обработки данных проходит через три-четыре пакета, каждый из которых поддерживается одним-двумя мейнтейнерами.
По данным IBM X-Force Threat Intelligence Index, среднее время между публикацией CVE и устранением в организации — 29 месяцев (общая метрика по всем CVE, без разбивки по типу зависимостей). Для транзитивных зависимостей вроде append-field срок предположительно ещё длиннее, хотя отдельных данных по ним отчёт не приводит. npm audit покажет проблему только после публикации advisory, а между обнаружением уязвимости и advisory может пройти несколько недель.
Команды, которые строят защиту вокруг npm audit и Dependabot, ловят CVE постфактум. Те, кто добавляют semgrep-правила на паттерны использования (а не на номера версий), ловят уязвимый код до публикации CVE. Разница: реактивный AppSec проверяет «есть ли у нас эта версия», проактивный — «есть ли у нас этот паттерн».
Если в проекте multer() без явных limits и upload-эндпоинт без аутентификации — это уязвимый паттерн вне зависимости от текущей версии. Следующий CVE в append-field может эксплуатировать тот же пробел. Запусти npm ls multer и semgrep-правило из раздела выше на своём проекте — если хоть одно срабатывание есть, у тебя та же проблема. На курсе WAPT в Codeby Academy разбор цепочек «middleware-зависимость-эксплуатация» идёт на живых лабах — можно отработать и semgrep-правила, и ручной код-ревью на реальных паттернах.
Эту тему и смежные навыки разбирают на практике в курсе «Профессия: инженер по безопасности приложений (AppSec)» Codeby Academy.