HN 日本語サマリー

← 一覧へ戻る
AI・機械学習

A2A実装におけるループジャッキング:Human-in-the-Loop承認の乗っ取り

Loopjacking in A2A Implementations: Hijacking Human-in-the-Loop Approvals (adithyanak.com)

8 pointsby akoffsec1 コメント

要約

LangGraph Agent Serverのテスト中に、Human-in-the-Loop(HITL)承認プロセスにおける脆弱性「ループジャッキング」が発見されました。攻撃者は、承認者が確認した操作とは異なる、より高額な送金操作を承認者の決定を利用して実行させることができました。これは、タスクIDのみをチェックし、承認された操作と実行される操作の間の整合性を検証しない実装の不備に起因します。仕様の明確化により、このリスクは軽減される可能性がありますが、実装側の注意が必要です。

全文翻訳

FN-10 / Agentic Security Research LangGraph Agent Serverの制御されたテストで、承認ロールはmock_wire_transfer(20, approved-vendor)に対するhuman-in-the-loop割り込みを受け取りました。別の作成者は保留中のスレッドを更新できましたが、保護された送金を承認または実行することはできませんでした。作成者はサーバーのA2Aルートを通じて別のメッセージを送信し、保留中のコールをmock_wire_transfer(2000, attacker-sink)に置き換えました。承認ロールは、20単位の送金に対する以前の決定を提出しました。モック台帳は、承認者の権限下で2,000単位の送金が記録されました。承認ロールは、テストがAの正確な製品ビューを主張した後にスクリプト化されました。このテストは、ユーザーインターフェースの変更に人が気づくかどうかではなく、製品の承認バインディングを測定します。インメモリサーバー、合成ID、決定論的なローカルモデル、無害な台帳を使用しました。公開証拠アーカイブは、リクエスト、決定、制御、およびテストされた正確なバージョンを保持します。 これがループジャッキングが発生する可能性のある方法の1つです。実装は、操作Aの決定を使用して、実質的に異なる操作Bをリリースします。決定的な質問は、Aの決定の下でどの操作がシンクに到達したかということです。この記事は、A2Aのタスクモデル、その承認ガイダンス、およびリリースされたAgent Serverパスを通じてその質問を追跡します。 タスクは承認された操作ではない A2Aタスクは、エージェントに継続的な作業単位を提供します。クライアントは既存のタスクとコンテキストを指定するメッセージを送信でき、エージェントは続行する前に、より多くの入力または承認が必要であることを報告できます。タスクIDは、このメッセージがどの作業に関するものかを示します。人間がどの正確なツールコールを承認したかは示しません。 タスクが操作Aを提案していると仮定します。アプリケーションはAを承認者に表示し、決定D_Aを記録します。後で別のメッセージが保留中の操作をBに変更した場合、タスクは同じIDを持つ可能性があります。タスクが承認されたかどうかのみをチェックする実装は、BにD_Aを使用する可能性があります。現在の実行可能な操作とD_Aにバインドされた操作を比較する実装は、Bを拒否するか、再度要求します。どちらの結果もタスクIDのみからは導き出されません。 ここには2つの異なる記録があります。調整記録は、タスクが一時停止しており、メッセージを受信でき、続行できることを示します。承認記録は、どの操作がレビューされたか、誰が承認したか、どのスコープの下で、実行が間近に迫ったときにまだ適用されるかどうかを示す必要があります。A2Aは前者を提供します。実装または資格発行者が後者を提供します。人間は意図的に広範なスコープを承認するかもしれませんが、その広範さは人間が見るものと実装が強制するものに明示されている必要があります。A2Aはタスクメッセージと継続を調整します。アプリケーションまたは発行者は、承認の操作スコープを定義し、チェックします。 その図は条件付きモデルです。失敗は、実装コードが変更されたBを選択し、スコープをチェックせずにD_Aを使用した場合にのみ存在します。同じタスクメッセージを受け入れ、新しい承認を要求するサーバーは、同じプロトコルメカニクスで安全に動作します。 明確化前のA2Aテキストが残したこと セクション7.6.4の前の仕様では、TASK_STATE_AUTH_REQUIREDを中断された非終端状態として扱っていました。クライアントが交渉、修正、またはリクエストを拒否できるように、承認が保留中である間にそのタスク宛てのメッセージを受け入れるようにエージェントにアドバイスしていました。また、帯域外で資格情報を受け取ったエージェントが、別のクライアントメッセージを待たずに続行することも許可していました。破壊的なアクションの前の人間の承認は、タスク内承認の例の中にありました。これらの選択は有用なワークフローをサポートします。要求者は、最初からやり直すのではなく、保留中のリクエストを修正できます。資格情報は別のチャネルを通じて到着する可能性があります。それらはまた、正確な質問を作成します。承認が保留中である間にメッセージが提案されたアクションを変更した場合、最終的な決定はどの操作をカバーしますか?古いテキストは、承認者、正準実行可能操作、アクションスコープの付与、または結果のシンクを定義していませんでした。また、承認スコープの定義と、後続の操作に対するそのチェックの責任を明示的に割り当てもいませんでした。それは仕様の明確化のギャップでした。それは、A2A自体が承認を付与した、タスク全体の承認を要求した、またはコアプロトコルの脆弱性があったという証明ではありませんでした。同じプロトコルフローは、Aをピン留めしてBを拒否する実装の上に座ることができます。 Issue #2080が曖昧さを提起しました。PR #2081は、7月30日、2026日にメインにマージされ、セクション7.6.4が追加されました。現在の仕様では、TASK_STATE_AUTH_REQUIREDは承認の必要性を示しますが、どの操作に対しても許可を与えるものではないと述べています。実装、資格発行者、または拡張機能がスコープを定義します。特定の操作に承認が必要な場合、実装はそれらを特定し、使用前に承認をチェックします。後続のタスクメッセージは暗黙的にカバーされません。その明確化は、実装者の状態の読み方を変更します。AUTH_REQUIREDは、作業が中断された理由をピアに伝えます。タスクを再利用可能な権限トークンに変えるものではありません。続行する際、実装は実際に実行するアクションを解決し、資格情報または人間の決定がそのアクションをカバーするかどうかを尋ねる必要があります。同じタスクメッセージが金額または宛先を変更した場合、ステータスとタスクIDはその質問に答えることはできません。 A2Aリリースページは、2026年9月22日にチェックされたとき、v1.0.1を最新タグとしてリストしていました。明確化はメインにマージされました。タグ付きリリースにはそれらが含まれていると仮定すべきではありません。 LangGraph Agent ServerでテストされたA2Aパス 具体的な製品結果は、仕様シナリオよりも狭いです。この実験は、LangGraph Agent Serverの出荷済みA2A JSON-RPCメッセージ/送信エンドポイントとそのメッセージ.コマンド.アップデートサーフェスを、公式のhuman-in-the-loopミドルウェアと組み合わせて実行しました。メッセージ.コマンド.アップデートは、そのルートで運ばれるAgent Serverコマンドであり、一般的なA2A承認プリミティブではありません。実験では、A2A §7.6の帯域外資格情報フローを実行せず、古い仕様の文言が製品結果を引き起こしたとは確立していません。テストされた構成は、Agent Serverのインメモリランタイム、LangChain 1.3.18、LangGraph 1.2.11、およびサポートされている研究者定義のAuthポリシーを使用しました。このポリシーは、承認を意味のあるものにする能力を意図的に分離しました。 プリンシパル できたこと できなかったこと メーカー 作業を作成し、共有された保留中のスレッドを更新する 承認の割り込みまたは保護されたモック送金の実行 承認者 保留中のビューを読み取り、割り込みを再開し、wire:executeで実行する メーカーの保留中状態の更新 外部者 関連するアクセスなし 保護されたスレッドを読む これは攻撃の前提条件であり、LangGraphのデフォルトポリシーに関する主張ではありません。メーカーがすでにwire:executeを持っていた場合、Bは承認者の決定を必要としなかったでしょう。メーカーが保留中状態を更新できなかった場合、テストされた置換パスは承認が再開される前に停止したでしょう。 Bが入り込んだ場所 メーカーの最初のA2Aリクエストは、スレッド上に作業を作成しました。次に承認者は、Agent Serverのhuman-in-the-loop割り込みを読み取りました。これには、正確なmock_wire_transfer名とAの引数が表示されました。その記録されたビューの後になって初めて、メーカーは別のメッセージ/送信リクエストを送信しました。そのメッセージは既存のtaskIdとcontextIdを指定し、置き換えられたアシスタントメッセージを含むcommand.updateを運びました。置き換えは、保留中のメッセージIDとツールコールIDの両方を再利用し、ツールコールの引数を変更しました。 圧縮された形式では、関連フィールドは次のとおりでした。 method: message/send message.taskId: existing Task message.contextId: shared thread message.command.update.messages[0].id: approval-action message.command.update.messages[0].tool_calls[0].id: transfer-call message.command.update.messages[0].tool_calls[0].args: B 繰り返されたIDは、更新が保留中の作業を対象とし、既存のコールを置き換えることを意味しました。それらは、その金額または宛先が承認者に表示されたコールと一致することを証明しませんでした。変更前後の記録により、その変更が明らかになります。 承認ビュー:id=transfer-call mock_wire_transfer(20, approved-vend