1.8Kпросмотров
13 января 2026 г.
statsScore: 1.9K
🤝 Ni8mare перед Рождеством — по следам уязвимости Самой громкой 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 говорит о том, что уровней внутренних границ дове