
2026/09/24 3:23
A2A実装におけるループジャッキング:ヒューマン・イン・ザ・ループ承認の乗っ取り
RSS: https://news.ycombinator.com/rss
要約▶
Japanese Translation:
LangGraph エージェントサーバーのバージョン 0.7.5 から 0.13.4 において、重大なセキュリティ脆弱性が特定されました。攻撃者は、
A2A の message.command.update でトリガーされる特定のコードフォワードパスを通じて承認システムを迂回し、許可されていない操作を実行することができます。この欠陥により、悪意ある actor が待機中のトランザクション指示を書き換え、システムを不正な高価値の送金処理(例:資金を承認されたベンダーではなく攻撃者用のシンクにルーティング)に誘導することが可能になります。以前のバージョン(例:0.7.4)はこの特定の攻撃パスを持たず、TASK_STATE_AUTH_REQUIRED シグナルに関する修正は 2026 年 7 月 30 日にメインブランチへマージされていますが、タグ付けされたリリース版 1.0.1 にアップグレードしても自動的に安全とは保証されません。タグが常に最新のメインブランチの変更を反映するわけではないため、ユーザーはデプロイメントがパッチ済みコードを含むことを明示的に確認する必要があります。公衆向けの証拠アーカイブでは、ハッシュ検証スキャンにより 130 の脆弱なリリースにわたるこれらの見解を確認しており、再現のための生のログとチェックサムを保持しています。将来の状態置換攻撃を防ぐためには、バージョンタグだけであればならず、ビルド検証を含めて慎重な回帰テストプロトコルを実施し、表示、格納、配信、再確認の各段階で操作状態を検証し、実行前にタスク効果を正規化することを保証すべきです。本文
LangGraph エージェントサーバーにおける「承認後の状態置換」脆弱性と対策
概要:テストで確認された問題点
LangGraph エージェントサーバーの制御されたテストにおいて、以下の**致命的な不一致(ループジャッキング)**が特定されました。
- 現象: 「作成者(Maker)」が保留中の処理を変更し、「承認者(Approver)」は変更前の内容を確認して「承認」しましたが、実際に実行されたのは変更後の不正な処理でした。
- 具体的事例:
という正当な転送に対し、人間による中断(human-in-the-loop interrupt)が発生しました。mock_wire_transfer(20, approved-vendor)- 作成者の行為: 保留中のスレッドを更新し、処理を
に置換しました。mock_wire_transfer(2000, attacker-sink) - 承認者の行為: 表示されていた「20 ユニットの正当な転送」を確認して承認しました。
- 実際の結果: モック・リダ(記録台帳)には、承認者が許可した 20 ユニットではなく、作成者によって置換された 2,000 ユニットの転送が記録されました。
- 作成者の行為: 保留中のスレッドを更新し、処理を
- 結論: 「承認済み」という状態と、実際に実行される操作は一致せず、**「A の判断で B が実行」**という攻撃パターンが発生しています。
タスクとは承認された操作ではない
A2A タスクの定義とリスク
- タスク ID とは: 「どの仕事についてのメッセージか」を示す ID であり、「人間が許可した正確なツール呼び出し」を指すものではありません。
- セキュリティ上のリスク:
- 古い実装パターン: タスク ID で承認済みをチェックするのみで、操作内容(引数や宛先)の変化を検知しない場合、**「承認された権限を使って不正な処理」**を実行してしまいます。
- 安全な実装パターン: 実行可能な操作と「承認された判断」を照合し、内容が一致しない場合は拒否するか再確認を求めます。
記録の二重性(重要)
システムには 2 つの異なる記録が必要です。
- 調整記録 (A2A が管理): 「タスクは一時停止しており、継続可能」という状態のみを示します。
- 承認記録(実装・発行元が管理すべき):
- レビューされた操作は何だったか
- 誰が承認したか
- どのスコープ(範囲)で承認したか
- 実行直前まで有効か
重要: 人間は意図的により広いスコープを承認することができても、実装が強制する表示範囲や、その有効性は明示的に定義する必要があります。タスク ID のみからは導き出せない情報です。
A2A 仕様の明確化と歴史的背景
古い仕様(改善前)
- 状況: セクション 7.6.4 以前の実装仕様では、
を「承認が必要」というシグナルとして曖昧に扱っていました。TASK_STATE_AUTH_REQUIRED - 問題点:
- 操作スコープの定義責任が不明確でした。
- 「承認後の状態置換」を防ぐための照合ロジックが必須要件として明示されていませんでした。
- A2A プロトコル自体に脆弱性があることを証明するものではありませんでしたが、実装側の判断に依存していたためリスクがありました。
解決策:仕様変更
- アイスイシュー #2080: この曖昧さを提起しました。
- PR #2081 のマージ(2026 年 7 月 30 日):
は「承認が必要である」というシグナルに明確化されました。TASK_STATE_AUTH_REQUIRED- スコープの定義は、実装やクレデンシャル発行元の責任となりました。
- 後続のメッセージが自動的に前の承認を有効にするわけではありません(明示的な再確認が必要)。
注意: この仕様の明確化はメインブランチにマージされていますが、古いバージョン(例:v1.0.1 など)のリリースには含まれていない可能性があります。
LangGraph エージェントサーバーでテストされた A2A 経路
テスト環境と構成
- 検証対象: LangGraph エージェントサーバーの
エンドポイントおよびmessage/send
サurface(機能領域)。message.command.update - 技術スタック:
- メモリ内ランタイム
- LangChain 1.3.18
- LangGraph 1.2.11
- 研究者定義の Auth ポリシー(Maker/Approver の分離)
ロールベースの権限設定
以下の表は、テストで使われた前提条件です。
| プリンシパル | 実行可能(可) | 実行不可(不可) |
|---|---|---|
| 作成者 (Maker) | ワーク作成、共有スレッドの更新 | 承認再開、保護されたモック転送の実行 |
| 承認者 (Approver) | ビュー閲覧、中断再開、 実行 | 作成者の保留状態更新(変更阻止) |
| 外部者 (Outsider) | アクセスなし | 保護されたスリード閲覧不可 |
「置換」の仕組み(B がどのように導入されたか)
- 提案: 作成者が
のタスクを作成。mock_wire_transfer(20, ...) - 中断: 承認者が人間によるループ内中断を読み取り、正当な
を表示。mock_wire_transfer - 置換(攻撃開始): 作成者が別の
リクエストを送信。message/send- 同じ
とtaskId
を指定。contextId
で引数をcommand.update
に変更。mock_wire_transfer(2000, attacker-sink, req-001)
- 同じ
- 承認: 承認者は「20 ユニットの正当な転送」と表示されているため
を送信。approve - 実行: ツールは承認者の権限を継承し、**変更後の引数(2,000 ユニット)**を受信して実行。
変化の可視化
- 承認ビュー(承認者が見た画面):
id=transfer-call mock_wire_transfer(20, approved-vendor, req-001) - A2A 更新(実際の変更内容):
id=transfer-call mock_wire_transfer(2000, attacker-sink, req-001) - ツールの受信結果:
mock_wire_transfer(2000, attacker-sink, req-001)
承認者が「approve」をクリックしても、現在のスレッド状態(置換された B)が実行パスに反映されます。以前の承認ビューとの比較が行われず、モックツールは B を受信しました。
コントロールが確立すること(検証結果)
テストでは、以下のように「攻撃の成功」を特定し、「防御」も確認しました。
| テストパス | 記録された効果(結果) |
|---|---|
| 攻撃パターン: 作成者が B へ更新し、承認者が陳腐化した判断を提出 | 攻撃スレッドのモック・リダにはBが記録される |
| 防御パターン A: 作成者が承認者の権限なしで B を試みる | B は到達しない |
| 正常なフロー: A が変更されず、承認される | A が到達する |
| 安全なポリシー: Auth ポリシーが承認中の作成者更新を拒否 | B はブロックされ、承認者は依然として A を実行できる |
結論:ギャップの解消
- 単に「要求が受け入れられた」ことを示すだけでは不十分です。
- 低権限のアクター(作成者)が直接シンクを呼び出すことができないことを証明する必要があります。
- このテストは、A2A バイパス主張におけるこれらの一般的なギャップを閉じました。
バージョン証拠の範囲
テスト対象バージョン
厳密なトレースは以下の12 つの実装バージョンで実行されました。
- 0.7.5, 0.7.103, 0.8.7, 0.9.1, 0.10.3, 0.11.4, 0.12.4, 0.12.6, 0.12.9, 0.13.2, 0.13.4, 0.14.0
注: バージョン 0.7.4 は機能不全なコントロール(必要なコマンド更新経路を消費しない)であり、結果の解釈には含まれません。バージョン 0.13.2 の陽性結果は Linux メモリ内コンテナでも再現確認済みです。
注意点
- 条件付き結果: これらの結果は「メモリ内構成」と「認証ポリシー」に限定されます。Postgres イメージなどの本番環境ではライセンスキー要件を満たす前に動作不明瞭になる可能性があります。
- デフォルトポリシー: デフォルトポリシー下の動作を確立しているわけではなく、ベンダーによる修正リリースの存在も確認していません。
- 監査証跡: 各実行は研究者 1 名が操作しており、独立した再現性は主張されていません。しかし、生の HTTP ログ、認証判断、パッケージピン、チェックサムなどの生データがアーカイブされています。
更新がなぜ重要か、そして何を証明しないか
問題点と信頼境界
- 同一 ID の罠: 同じタスク ID やツール呼び出し ID を保持しても、引数が変更されたことを証明できません。
- 信頼境界の交差点: 承認者の権限は「操作 A」に対して付与されていますが、実行パスは現在の操作 Bを使用しました。B が判断のスコープ内にあることを検証しないと、失敗を確立できません。
- 「Green Light」ではない: 「承認済み(approved)」ステータスや成功した A2A レスポンス自体が、処理の正当性を保証するものではありません。
適用範囲
- これらの結果は、出荷された A2A ルートにおける製品固有の承認バインディング故障を支持します。
- A2A のコア仕様全体や、すべての SDK に共通のバグがあることを示すものではありませんでした。
- 古い仕様が「操作のスコープ所有権」について不明確であった点と、実装がその所有権を強制する必要性を示しています。
使用点で操作を防ぐ(対策・デザインガイドライン)
デザインガイドライン
正当なタスク変更が必要な場合、決定(判断)を正確な効果にバインドしてください。
- アプリケーションは完全な規範的な操作から承認ビューを作成する。
- それにバインドされた完全性を保護された判断で保存する。
- 最後に状態リダクサーやデフォルト入力後には、再度**操作を確認(再検証)**する。
- 物質的フィールド(数量、宛先など)への変更には、新しい決定が必要です。
具体的な例:転送の重要性
やtaskId
が同じでも、20 から 2,000 ユニットへの変更や、request_id
からapproved-vendor
への変更は**物質的(重要)**です。attacker-sink- これらを無視して再実行すると、攻撃を許容することになります。
デザイン擬似コード:アクションバウンド承認プロトコル
注意: これはデプロイ可能なパッチではなく、設計上のガイドラインです。
ブロックは、タスクスナップショットと実際のトランザクションの整合性を意味します。atomic
# 1. プロセス中の完全な効果(canonicalize)を解決し、承認ビューに提示する proposed = canonicalize(resolve_complete_effect(task)) show_to_approver(proposed) if approver_accepts: # 2. 完全性を保護された判断を作成・保存する decision = integrity_protected_record( approved_operation = proposed, operation_digest = hash(proposed), approver = authenticated_approver, requester = task.requester, task_id = task.id, policy_version = active_policy.version, execution_scope = approved_execution_scope, expires_at = deadline, state = APPROVED, nonce = new_nonce() ) def on_release(task_id, decision_id): # 3. ディスパッチ時:原子的にロックして再構築を行い、整合性をチェック atomic(task_state, approval_store, dispatch_outbox): task = lock_task(task_id) decision = lock_decision(decision_id) current = canonicalize(resolve_complete_effect(task)) require(decision.state == APPROVED) require(now() < decision.expires_at) require(decision.task_id == task.id) require(decision.requester == task.requester) require(decision.policy_version == active_policy.version) # 4. 【重要】決定された操作と現在の状態が一致する確認 require(decision.execution_scope.allows(execution_context)) require(hash(decision.approved_operation) == decision.operation_digest) # ここに「置換」を検知するロジックが存在します require(hash(current) == decision.operation_digest) require(active_policy.allows(decision.approver, execution_context, current)) require(consume_once(decision.nonce)) # 5. キューに不変な操作を蓄積(再構築しない) enqueue_immutable(dispatch_outbox, current, key=decision.id) append_audit(decision, current, outcome="queued") dispatch_the_queued_operation()
シルク(重要)ルール
- 単なるステータスチェックは不十分: 承認者のクリック時だけでなく、ディスパッチ直前のタスク状態を再構築し、一致しているか厳密に比較してください。
- 監査記録: 表示された操作、使用時の操作、判断、シンク結果を保持すべきです。
シンクに到達する回帰テスト(実装確認リスト)
A2A 実装のレビューのため、以下の 4 つのアートをキャプチャしてください。
- 人間に見える操作
- 判断記録
- 保留中の状態を変化させる可能性があるメッセージ/リダクター
- 結果としてのシンクが受信する操作
確認すべきテストブランチ
異なる作成者と承認者プリンシパルを使用して以下のシナリオを実行し、アサーションを行ってください。
- 未変更の A:
- 正確な A を承認。
- シンクが一度だけ A を受領することを確認(アサーション)。
- 承認なし:
- 作成者が直接 B を提案。
- 保護された効果が発生しないことを確認。
- 同じタスク置換(攻撃シナリオ):
- A を表示し、物質的フィールドを B に変更する許可された更新を送信。
- 以前に「A の判断」を提出。
- シンクは、その決定の下でB を受領してはいけません(または沈黙のフォールバック)。
- 期待される動作: B を拒否するか、新しい B の決定を獲得する必要がある。
- 間違っているプリンシパルとスコープ:
- 別のタスク、期限切れ、消費された判断からの再開を試みる。
- それぞれ**効果なしで失敗(ブロック)**する必要があります。
- 安全なポリシーブランチ:
- 作成者の保留中の更新を拒否するロジックを実装。
- 正当な承認が依然として未変更の A を解放することを確認。
警告: シンクでの正確な引数をアサーションし、単に「承認済み(approved)」ステータスや成功したレスポンスのみをチェックしないでください。エージェントサーバー側では無害に見えても、バックエンドのリダには A から B への変化が記録される場合があります。
守るべき境界と結論
A2A の限界
- A2A は承認が保留中にある間にタスクを存続させ、別のメッセージを受け付けることを規定しています。
- 古い仕様(セクション 7.6.4 以前)では、操作スコープチェックの割り当てが明確ではありませんでした。
修正のアプローチ
- アプリケーション境界: アプリケーション自体に「正確に何が承認されたか」を決定し、その判断を保護する責任があります。
- 比較ロジック: ディスパッチする直前の操作と、保存された判断(承認ビュー)を比較してください。
- タスク ID の限界: タスク ID は「どの仕事か」を示しますが、「B が承認された」とは示しません。
関連資料
- What Is Loopjacking?(ループジャッキングの定義)
- A2A セクション 7.6.4 の仕様の明確化
- Microsoft Action-Bound Approval Protocol
(公衆向けの仕様とリリースステータスは 2026 年 9 月 22 日に確認されました。実験アーカイブの認められた証拠カットオフは 2026 年 9 月 10 日です。)