Самой громкой CVE за каникулы стала, пожалуй, брендово-трендовая-и-вот-это-всё CVE-2026-21858 aka Ni8mare, получившая CVSS 10.0 благодаря возможности развития атаки на неё до полноценного RCE. Саму уязвимость не разобрал разве что только ленивый <автор этого канала>. Вот хороший разбор с красивыми картинками и примерами кода, и, опять-таки — повторяться, теперь уже, смысла нет.
Причина этой уязвимости, тут же названная «Content-Type Confusion» теми, кому совершенно не жаль составителей таксономий, заключается в ошибках обработки HTTP-запросов с различными
Content-Type заголовками в webhook-эндпоинтах n8n: сервер необоснованно доверяет данным из req.body.files даже когда заголовок Content-Type не указывает на multipart/form-data, что позволяет атакующему подменить содержание и структуру тела запроса. Это приводит к тому, что функции обработки считают произвольно сформированные данные «загруженными файлами» и используют их для чтения файлов на сервере. Используя это и функциональность прочих модулей, доступных в n8n, атакующий может развить атаку вплоть до RCE.Цепляет в уязвимости то, что здесь явно теряется соблюдение в коде границы доверия, но настолько неявным способом, что заметить это, просто читая код глазами или SAST'ом — не так уж и просто. Вот близкий к оригиналу псевдокод, иллюстрирующий уязвимость этого типа:
// Middleware: разбор тела запроса в вебхуке
function parseRequestBody(req) {
if (req.headers['content-type'].startsWith('multipart/form-data')) {
// Парсим форму и файлы (через, например, Formidable)
req.body = parseFormData(req); // сформирует req.body.files для файлов
} else {
// Парсим JSON или другие типы как обычное тело
req.body = parseBody(req); // напрямую десериализует тело в req.body
}
}
// Обработчик webhook формы (уязвимая версия)
app.post('/form-webhook', (req, res) => {
parseRequestBody(req);
const result = prepareFormReturnItem(req.body);
// ...
});
// Функция обработки загруженных файлов
function prepareFormReturnItem(body) {
for (const fileId in body.files) {
// Скопировать файл из временного пути в постоянное хранилище
copyBinaryFile(body.files[fileId].filepath, uploadDir);
}
// ... вернуть результат для workflow
}
Да,
copyBinaryFile как бы намекает, что это потенциально опасная операция копирования файлов. Но SAST, не знающий о деталях работы Formidable, как минимум, здесь даст фолз+, на ветке с multipart/form-data, а человек, проводящий триаж/ревью — вообще забьет на обе сработки, т.к. по логике — копирование файлов тут норм, ведь их исходные пути мы получаем от парсера (ведь только от парсера же, да? 😬), выполняющего здесь ещё и роль доверенного санитайзера.🖥 Что делать разработчикам?
• Структурные входные данные должны валидироваться по схеме конкретного кейса бизнес-логики, следуя принципу fail-closed, прежде, чем начнется работа с их полями (даже их валидация).
• Не стоит смешивать в одном потоке выполнения несколько логических кейсов. В данном случае — следовало бы разнести по разным эндпоинтам работу с разными типами контента (облегчает задачу SAST, делает счастливыми триажеров и ревьюеров — сплошной профит). Но, если уж смешались, то п.1 должен быть корректно реализован для всех веток выполнения.
• Инварианты и гарантии, предоставляемые используемыми парсерами, валидаторами и санитизаторами стоит изучить досконально. Даже в «нормальной» ветке с
multipart/form-data, то, что formidable гарантирует загрузку по безопасным путям относительно options.uploadDir — нужно знать, а не предполагать. А ещё лучше — лишний раз убеждаться в этом, прежде, чем работать с полученными результатами. Почему?• Потому что, Defense in Depth через многоуровневую модель угроз никто не отменял. То, что, например, через обычную читалку файлов стало возможным вытащить
.n8n/database.sqlite говорит о том, что уровней границ доверия у n8n просто не было.⚠ TL;DR: читаем разбор Ni8mare, делаем выводы, сравниваем с написанным выше ✍️
#уязвимости