AIオーケストレーションの破壊 Part 2: n8nのHITLチャットセッションを乗っ取る

編集部注: 本稿はAIオーケストレーションプラットフォームに関する3部構成シリーズの第2回です。第1回ではLangflowを取り上げ、第3回ではActivepiecesを取り上げます。本研究はもともとBlueHat IL 2026にて「Exploiting AI Orchestration Zero-Days via PostgreSQL Internals」と題した講演で発表されたものです。スライドおよび録画も現在公開されています。

n8nは、現在利用されているAIオーケストレーションプラットフォームの中でも特に人気の高いものの一つです。Part 1で取り上げたLangflowとは全く異なるスタックで、キューを利用したワーカーを持つTypeScriptアプリという構成ですが、コンセプトは同じです。特定のイベント、データ変換、外部連携、AIモデルなどを表す部品をドラッグ&ドロップするだけで、わずか半日程度でAIアプリケーションや自動化処理を構築できます。

攻撃対象領域のマッピング

n8nはNestJSスタイルのTypeScriptアプリです。ルートは@RestController@Post@Getといったデコレータで宣言されます。認証モデルは単一のミドルウェアチェーンに集約されています。

n8nのような形状のアプリでは、ルートが未認証のまま公開されてしまう経路は2つ考えられます。1つはデコレータシステム自体、もう1つはそこから外れて登録されたルート全般です。

認証ミドルウェアはAbstractServer内で一度だけ組み込まれています。

つまり、デコレータで定義されたルートが未認証になるのは、明示的にskipAuth: trueを指定した場合のみです。これはn8nの優れた設計判断と言えます。このフラグ(および該当ハンドラが使用するAuthlessRequest型)をgrepで調べたところ、/loginやヘルスチェック、/setupなど、短くごく平凡なリストが得られました。

Langflowのときと同様、認証ロジック自体を調査してみましたが、コア部分はしっかりしていました。n8nはJWTを適切に利用しています。

ただし、一つ気になる点がありました。ある setup エンドポイントがskipAuth: trueとして宣言されているにもかかわらず、AuthenticatedRequest型として型付けされていたのです。

この「型とフラグの不一致」は、TypeScriptアプリで見つけたいバグの典型的な形です。型システムが一つのことを語り、ミドルウェアチェーンが別のことを語っているわけです。この事案では、ハンドラは実際にはreq.userに依存していなかったため、これは脆弱性ではなく単なるミスでした。とはいえ、この種のパターンは至るところをgrepしてチェックする価値があります。今回はここからは何も出てきませんでしたが。

しかし、デコレータで定義されたルートだけがすべてではありませんでした。AbstractServerは、コントローラの配線とは別に、app.use(...)app.all(...)を使って大量のルートを直接登録しています。これらはskipAuth / createAuthMiddlewareのパイプラインを経由しないため、それぞれが独自に認証を実装するか、あるいは意図的に公開されている必要があります。その一覧は次の通りです。

そして、実際に問題となったものがこちらです。

これで登録処理はすべてです。ミドルウェアなし、認証への依存なし、チェックなし。ただ「求めてきた相手のためにチャットセッションを開始する」だけです。

ライブチャットセッションの未認証乗っ取り

今回の最大の発見は、Human-in-the-Loop(HITL)方式のエージェント型ワークフローで人間とのライブ会話を成立させるn8nのChatノードを利用しているユーザーは誰でも、未認証の攻撃者から会話を盗聴されたり、会話にメッセージを注入されたりする恐れがあったという点です。しかも識別子は列挙可能であるため、あるインスタンス上で稼働中のHITLチャットセッションはすべて発見できてしまいます。

Chatノードとは何か

n8nのChatノードは、エージェント型ワークフローの作成者がユーザーと双方向のやり取りを行えるようにする機能で、通常はAIエージェントのユーザー向けインターフェースとして使われます。このノードはメッセージを送信し、応答を待ち、また送信する、という流れを繰り返します。内部的には、「ユーザーの次のメッセージを待つ」という処理はワークフローの実行を一時停止させます。ユーザーが返信すると、実行は中断した箇所から再開されます。

この双方向のやり取りの通信路となっているのが、/chatで提供されるWebSocketです。フロントエンドは次のように接続します。

executionIdは一時停止中のワークフロー実行を識別し、sessionIdはその中のチャットセッションを識別します。この2つが組み合わさることで、ソケットがどの会話に紐付いているかが決まります。

バグの内容

ここには正確に3つの問題があり、それらが重なり合っています。

  1. /chatには認証がない。先述の通り、登録処理は文字通り app.use(‘/chat’, …) です。デコレータシステムの外にあるためskipAuthフラグは存在せず、ハンドラ内にも手動の認証チェックはありません。
  2. executionIdは列挙可能な整数である。n8nは連番の整数を実行IDとして使用します。WebSocketを開く際に有効なIDを事前に知っている必要は一切なく、単純にループで試せば済みます。
  3. sessionIdはクライアントが名乗った値がそのまま使われる。サーバーはセッションをユーザーアカウントに紐付けていません。チャット層から見れば、あるsessionIdで最初に接続してきた者が、そのセッションの持ち主ということになります。

これらのバグを組み合わせると、攻撃は単純明快になります。実行IDを列挙し、現在入力待ちになっているチャットセッションを見つけ、そこに接続すればよいのです。

稼働中のセッションを見つける

この列挙処理では、「実行Nは現在チャット入力待ちである」ことを、「実行Nは存在しない」や「実行Nは数時間前に終了した」といった状態と区別する手段が必要です。そのシグナルを提供するのが、先ほど示したのと同じapp.all(...)ブロックを通じて登録されている、隣接ルートの/form-waiting/:executionId/:suffixです。このURLへのリクエストは、実行が入力待ちで停止している場合は文字列"waiting"そのものを返し、それ以外の場合は別の値を返します。整数を順に試し、応答を記録していけば、今まさに標的にできる実行がどれかを判定できるオラクルが手に入ります。さらに、新しいセッションを待ち受けたい場合は、現在存在する最大のIDの次のIDを監視するだけで済みます。

/form-waiting自体は壊れているわけではありません。ポーリングを行うクライアントが、待機中のフォームが完了したかどうかを確認できるようにするために存在しています。しかし攻撃者の視点から見れば、これはワークフローの実行状態を未認証のネットワークに漏らしてしまうものであり、まさにどの実行をいつ狙うべきかを攻撃者に教えてしまうものです。

会話の乗っ取り

稼働中のexecutionIdさえ手に入れば、WebSocketを開くだけで簡単にそのセッションに接続できます。

このソケットは双方向通信であり、通信プロトコルはコード上に記載されています。ユーザーメッセージのJSONフレームは{"sessionId": ..., "action": "sendMessage", "chatInput": ...}のような形式です。

相手側のフローの内容次第では、攻撃者はエージェントの出力(AIが実際のユーザーに送り返そうとしているツールの実行結果、要約、取得したコンテキストなどを含む)を読み取ったり、新たなユーザーメッセージを注入して会話を意のままに誘導したり、新たな接続に表示される過去のやり取りをそのまま再生したりすることができます。

これを実演してみましょう。以下のデモでは、画面上半分で正規ユーザーがn8nのUIを通じてAIエージェントとやり取りしている様子が確認でき、下半分では上述した攻撃のシミュレーションが確認できます。攻撃者がまずIDを列挙してアクティブなセッションを見つけ出し、このセッションで送受信されたチャットメッセージを読み取り、さらには自らメッセージを注入してセッションを操作する様子までご覧いただけます。

デモ動画を見る

この脆弱性はn8nチームによって修正され、CVE-2026-42228(深刻度: Moderate、CVSSスコア6.3)が割り当てられました。

n8nのPythonサンドボックスからの脱出

今回の調査では、n8nのPythonサンドボックスに関する短いエピソードも扱いました。n8nでは、プロジェクト内でカスタム処理ロジックを実現するために、ユーザーがPythonコードを入力できる便利な機能が用意されています。

この機能を安全に実現するため、ユーザーが入力したPythonコードは強化されたインタプリタ上で評価されます。この強化は、互いに補完し合う2つの方法で行われています。

  1. まずコードは、静的コード解析を使って禁止パターンの有無をチェックするアナライザーを通過します(SecurityValidatorを参照)。禁止パターンの一部を挙げると次の通りです。

    1. インポート

    2. 特定の名前のブロック(eval, exec, compile, __import__, __globals__, __class__, __subclasses__など)

  2. コードは、print関数と限定されたサブセットの__builtins__のみを含むglobalsを使って、PythonのexecでExecutionされます(完全なリストはこちらを参照)。ブロックされる名前のリストには次のものが含まれます。

    1. eval, exec, compile, open, getattr, object, type, globalsその他多数

この仕組みの狙いは、ユーザーがPythonでロジックを定義できるようにしつつ、機微なリソースへのアクセスは一切許可しないというものです。その防御は徹底されているものの、Pythonコードの実行を一部許容しながら任意コード実行を防ぐというのは、極めて難しい課題です。

この2つの防御はいずれも、攻撃者が何らかの変数をフィルタされていない本来のglobalsに向けさせることに成功すれば破綻します。通常、フィルタされていないglobalsを取得するのは容易です。print関数は依然として利用できるため、print.__builtins__["exec"]を実行するだけでexec関数を取得し、任意のコードを実行できてしまうはずです。

しかし、__builtins__メンバーへのアクセスは、ドット(.)を使う場合でもgetattrを使う場合でも、静的解析によってブロックされています。しかし今回の調査では、当時のアナライザーがモデル化できていなかったPythonの機能、すなわち構造的パターンマッチングを利用したバイパス手法を発見しました。match/caseは、クラスパターンのレベルで構造的マッチングによって属性を束縛しますが、そのASTノードの種類であるMatchClassは、そもそもビジターの巡回経路に含まれていなかったのです。

これをどう使ってフィルタされていない__builtins__にアクセスするのか、その例を示します。

builtins変数には、フィルタされていないbuiltinsが格納されています。

唯一の前提条件は、matchパターンのクラスとして使うobjectへの参照(上記のcase内での使用箇所を参照)でしたが、objectはブロックされる名前のリストに含まれていました。

これを回避する方法は、歴史上あらゆるPythonサンドボックスが突破されてきたのと同じ方法です。任意の具象クラスの型メタデータにはobjectがぶら下がっています。

list.__format__は組み込みメソッドであり、組み込みメソッド上の__objclass__は、そのメソッドが定義されているクラスを返します。__format__の場合、それはobjectです。このobjectを手にすれば、matchパターンによるバイパスによって、__builtins__をそのままローカル変数に束縛できます。

MatchClassノードはビジターの巡回対象になっていなかったため、キーワード属性は他のすべてのチェックをすり抜けていました。ここまでくれば、builtinsからevalとexecを取得し、print.__globals__を実際のモジュールglobals辞書としてexecに渡すのは容易です。

この完全なペイロードをCodeノードに貼り付けると、ルートディレクトリの一覧が返ってきます。

n8n Cloud上でこれをテストしたところ、成功しました。

その後、報告書を書くために取り掛かったのですが、その前にもう一度テストしてみたところ、動作しなくなっていました。アナライザーには現在visit_MatchClassメソッドが追加されており、各パターンのキーワード属性を巡回してブロック対象の名前を検出することで、この抜け穴をふさいでいます。

n8nチームが私のテストを検知したのか、あるいは別の研究者がすでに報告していたのかは、最後まで分かりませんでした。しかしこれは興味深い脆弱性とエクスプロイトであり、そこから学べることがあると考えています。

ここで得られる教訓は「ブロックリストを使え」ということではありません。静的アナライザーの精度は、巡回対象とするASTノードの種類の網羅性と、識別できるパターンの範囲によってのみ決まります。しかし、Pythonは属性を束縛したり参照したりする新たな構文形式を追加し続けています。match/caseはその一例に過ぎず、今後の言語仕様の追加も同様の抜け穴を生み続けるでしょう。AST解析をベースとするサンドボックスは、文法が拡張されるたびに再監査する必要があります。Pythonコードのグローバルなフィルタリングや静的解析だけでは、決して十分ではありません。だからこそn8nは、上記の対策に加えて、コードを分離されたタスクランナー内で実行する仕組みもサポートしています。信頼できるセキュリティ境界を実現するには、この機能を有効にすべきです。

開示タイムライン

  • 2026年2月8日 — n8nチームへ脆弱性を報告
  • 2026年4月20日 — 脆弱性が修正される

Ori Lahav氏はRubrik Zero Labsのセキュリティ研究者です。本稿で紹介した研究は、新興のAIインフラストラクチャのセキュリティに関するRubrik Zero Labsの取り組みの一環として実施されました。

翻訳元: https://zerolabs.rubrik.com/blog/breaking-ai-orchestration-part-2-hijacking-n8n-hitl-chat-sessions

本記事は zerolabs.rubrik.com の記事を翻訳・要約したものです。