この記事で分かること
- AIエージェントにおける Authorization-Execution Gap(認可-実行ギャップ) とは何か
- 認可された操作が実行までの間に改ざん・逸脱される 4つの攻撃パターン
- 従来のセキュリティ概念「TOCTOU」との関係と、AIエージェント固有のリスク
- Execution Integrity(実行完全性) を検証する実践的な設計パターン
- PreToolUse / PostToolUse フックを活用した実装例
はじめに
AIエージェントにツールの使用を許可する。データベースへの読み取りアクセスを認可する。ファイルの編集を承認する——こうした「認可」は、AIエージェントを安全に運用するための基本的な制御です。
しかし、「認可した操作」と「実際に実行される操作」が本当に一致しているか、あなたは検証していますか?
従来のセキュリティでは、権限チェックと操作実行の間に時間差がある場合に発生する脆弱性を TOCTOU(Time-of-Check to Time-of-Use) と呼びます。AIエージェントの世界では、この問題がさらに複雑な形で顕在化しています。ユーザーが「この操作を許可します」とボタンを押した瞬間と、エージェントが実際にツールを呼び出す瞬間の間には、ツール定義の動的変更、パラメータの改変、コンテキストの汚染といった介入の余地があるのです。
本記事では、この「認可」と「実行」の間に潜むギャップを Authorization-Execution Gap(認可-実行ギャップ) として体系化し、実行完全性(Execution Integrity)を検証するための設計パターンを解説します。
前提知識
TOCTOU(Time-of-Check to Time-of-Use)とは
TOCTOU は、「条件を確認した時点」と「その条件に基づいて操作を実行する時点」の間にタイムラグがある場合、その間に条件が変更されてしまう脆弱性です。
身近な例で説明します。映画館で年齢確認(チェック)をしてチケットを買い、上映開始まで30分の待ち時間がある場合、その間にチケットを別の人に渡すことができてしまいます。「年齢確認済み」というチェック結果と、「映画を観る」という実行が分離しているために起こる問題です。
AIエージェントにおける認可の仕組み
AIエージェントの多くは、以下のような認可メカニズムを持っています。
| 認可レベル | 説明 | 例 |
|---|---|---|
| ツール単位の承認 | 特定のツールの使用を許可/拒否する | 「Bashツールの実行を許可しますか?」 |
| 操作単位の承認 | 特定の操作内容を確認して許可する | 「このファイルを編集してよいですか?」 |
| ポリシーベースの自動承認 | 事前定義のルールで自動判定する | 読み取りは自動許可、書き込みは要確認 |
これらはすべて「チェック時点」の情報に基づいて判断しています。しかし、AIエージェントの実行環境では、チェックと実行の間に多くの介入ポイントが存在します。
Authorization-Execution Gap の4つの攻撃パターン
パターン1: Rug Pull — 承認後のツール定義変更
Rug Pull は、MCP(Model Context Protocol)環境で特に深刻な攻撃です。初回の承認時には無害なツール定義を提示し、承認後に定義を動的に変更します。
この攻撃は、MCPクライアントがツール定義をキャッシュせず、毎回サーバーから取得する場合に成立します。ユーザーは「安全な計算ツール」を承認したつもりですが、実際には改変後の「データ窃取ツール」が実行されます。
パターン2: パラメータ改変 — 認可されたツールの悪用
ツール自体は正当に認可されていても、呼び出し時のパラメータが認可時の意図と異なるケースです。
// ユーザーが認可した意図: プロジェクトの設定ファイルを読む
// 認可メッセージ: "read_file ツールの使用を許可しますか?"
// ユーザーの認識: "設定ファイルを読むんだな、OK"
// 実際に実行されるコール(間接プロンプトインジェクション経由)
read_file({ path: "/etc/passwd" })
// または
read_file({ path: "~/.aws/credentials" })
ここでの問題は、認可が「ツール単位」で行われ、「パラメータ単位」では行われないことです。read_file の使用を許可した時点で、エージェントはどのファイルでも読めてしまいます。これは、「車の運転を許可した」ことが「どこへでも行ってよい」を意味してしまうようなものです。
パターン3: コンテキストウィンドウ汚染による意図の逸脱
認可時点ではエージェントの意図が正当でも、認可後にコンテキストが汚染され、実行時の意図が変わるパターンです。
この攻撃の特徴は、認可の仕組み自体は正常に機能していることです。ユーザーは write_file を許可し、エージェントは write_file を実行します。しかし、「何を」書き込むかが認可時の意図と異なるのです。
パターン4: マルチステップ操作における権限の累積的逸脱
複数ステップにわたるタスクで、各ステップは正当に見えるが、全体として認可された範囲を超えるパターンです。
個々の操作は「ファイル読み取り」「HTTP通信」「ファイル書き込み」と、いずれも単独では無害に見えます。しかし、3つを組み合わせるとデータの外部流出が成立します。各ステップを個別に承認する仕組みでは、この複合的な脅威を検知できません。
Execution Integrity(実行完全性)とは
Execution Integrity は、「認可された操作が、認可された通りに、認可された範囲内で実行されること」を検証する設計原則です。
従来の認可モデルが「チェック時点」の静的な判断であるのに対し、Execution Integrity は「チェック → 実行 → 検証」のライフサイクル全体をカバーします。
実践的な Execution Integrity 検証パターン
パターン1: ツール定義のピン留めとハッシュ検証
Rug Pull攻撃への対策として、ツール定義を承認時点で固定し、実行時に変更がないことを検証します。
import hashlib
import json
class ToolDefinitionPinner:
"""ツール定義を承認時点でピン留めし、
実行時に改ざんを検知するクラス"""
def __init__(self):
self._pinned_definitions: dict[str, str] = {}
def pin_on_approval(self, tool_name: str, definition: dict) -> str:
"""ユーザー承認時にツール定義のハッシュを記録する"""
canonical = json.dumps(definition, sort_keys=True)
digest = hashlib.sha256(canonical.encode()).hexdigest()
self._pinned_definitions[tool_name] = digest
return digest
def verify_before_execution(
self, tool_name: str, current_definition: dict
) -> bool:
"""実行前にツール定義が変更されていないことを検証する"""
if tool_name not in self._pinned_definitions:
raise ValueError(f"未承認のツール: {tool_name}")
canonical = json.dumps(current_definition, sort_keys=True)
current_digest = hashlib.sha256(canonical.encode()).hexdigest()
return current_digest == self._pinned_definitions[tool_name]
パターン2: パラメータバウンダリの明示的定義
ツール単位ではなく、パラメータの許容範囲を事前に定義し、実行時に逸脱を検知します。
from dataclasses import dataclass
@dataclass
class ParameterBoundary:
"""パラメータの許容範囲を定義するクラス"""
allowed_paths: list[str] # アクセス可能なパス
denied_patterns: list[str] # 拒否するパターン
max_content_length: int # 最大コンテンツ長
require_confirmation_for: list[str] # 再確認が必要な条件
class BoundaryEnforcer:
"""パラメータが認可された範囲内かを検証するクラス"""
def __init__(self, boundary: ParameterBoundary):
self._boundary = boundary
def validate(self, tool_name: str, params: dict) -> tuple[bool, str]:
if tool_name == "read_file":
return self._validate_file_access(params.get("path", ""))
if tool_name == "bash":
return self._validate_command(params.get("command", ""))
return True, "OK"
def _validate_file_access(self, path: str) -> tuple[bool, str]:
import os
resolved = os.path.realpath(os.path.expanduser(path))
# 拒否パターンのチェック
for pattern in self._boundary.denied_patterns:
if pattern in resolved:
return False, f"拒否パターンに一致: {pattern}"
# 許可パスのチェック
if not any(resolved.startswith(p)
for p in self._boundary.allowed_paths):
return False, f"許可されたパス外: {resolved}"
return True, "OK"
def _validate_command(self, command: str) -> tuple[bool, str]:
dangerous = ["rm -rf", "curl", "wget", "nc ", "dd "]
for cmd in dangerous:
if cmd in command:
return False, f"危険なコマンドを検出: {cmd}"
return True, "OK"
パターン3: 操作チェーン解析による複合脅威の検知
個々のツール呼び出しではなく、一連の操作をチェーンとして評価し、複合的なデータ流出パターンを検知します。
from collections import deque
from dataclasses import dataclass, field
@dataclass
class ToolCall:
tool_name: str
params: dict
timestamp: float
class OperationChainAnalyzer:
"""操作チェーンを解析し、
複合的な脅威パターンを検知するクラス"""
# 危険な操作チェーンのパターン定義
DANGEROUS_CHAINS = [
# 読み取り → 外部送信(データ流出)
{
"pattern": ["read_file", "web_fetch"],
"risk": "機密データの外部流出",
"severity": "critical",
},
# 読み取り → 書き込み(データの改ざん中継)
{
"pattern": ["read_file", "read_file", "write_file"],
"risk": "複数ファイル読み取り後の不正書き込み",
"severity": "high",
},
# 環境情報収集 → 外部送信
{
"pattern": ["bash", "web_fetch"],
"risk": "環境情報の偵察と流出",
"severity": "critical",
},
]
def __init__(self, window_size: int = 10):
self._recent_calls: deque[ToolCall] = deque(maxlen=window_size)
def record_and_analyze(
self, call: ToolCall
) -> list[dict]:
"""操作を記録し、危険なチェーンを検知する"""
self._recent_calls.append(call)
tool_sequence = [c.tool_name for c in self._recent_calls]
alerts = []
for chain in self.DANGEROUS_CHAINS:
if self._is_subsequence(chain["pattern"], tool_sequence):
alerts.append({
"risk": chain["risk"],
"severity": chain["severity"],
"detected_sequence": tool_sequence[-len(chain["pattern"]):],
})
return alerts
@staticmethod
def _is_subsequence(pattern: list[str], sequence: list[str]) -> bool:
it = iter(sequence)
return all(item in it for item in pattern)
パターン4: Pre/Post フックによるリアルタイム検証
Claude Code のHooksシステムのように、ツール実行の前後にインターセプトポイントを設け、リアルタイムで検証します。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Write|Edit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/verify-execution-integrity.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/audit-execution-result.sh"
}
]
}
]
}
}
PreToolUse フックの実装例です。
#!/bin/bash
# .claude/hooks/verify-execution-integrity.sh
# ツール実行前にパラメータの妥当性を検証する
input="$(cat)"
tool_name="$(echo "$input" | jq -r '.tool_name')"
tool_input="$(echo "$input" | jq -r '.tool_input')"
case "$tool_name" in
Bash)
command="$(echo "$tool_input" | jq -r '.command')"
# 機密ファイルへのアクセスと外部送信の組み合わせを検知
if echo "$command" | grep -qE '(cat|less|head).*(passwd|shadow|credentials)' &&
echo "$command" | grep -qE '(curl|wget|nc)'; then
echo '{"hookSpecificOutput":{"permissionDecision":"deny","permissionDecisionReason":"機密ファイル読取と外部送信の組合せを検知しました"}}' >&2
exit 2
fi
;;
Write|Edit)
file_path="$(echo "$tool_input" | jq -r '.file_path // .path')"
# 設定ファイルや認証情報の変更をブロック
if echo "$file_path" | grep -qE '\.(env|pem|key|credentials)$'; then
echo '{"hookSpecificOutput":{"permissionDecision":"ask","permissionDecisionReason":"認証関連ファイルの変更には追加確認が必要です"}}' >&2
exit 2
fi
;;
esac
exit 0
Execution Integrity 設計チェックリスト
実際のプロジェクトで Execution Integrity を導入する際のチェックリストです。
| カテゴリ | チェック項目 | 対応パターン |
|---|---|---|
| ツール定義 | 承認時のツール定義をハッシュで記録しているか | パターン1 |
| ツール定義 | 実行前にハッシュを再検証しているか | パターン1 |
| パラメータ | パラメータの許容範囲を明示的に定義しているか | パターン2 |
| パラメータ | パスのトラバーサル(../ 等)を検知しているか |
パターン2 |
| 操作チェーン | 読み取り→外部送信のチェーンを監視しているか | パターン3 |
| 操作チェーン | 操作の頻度異常(短時間の大量読み取り等)を検知しているか | パターン3 |
| フック | 実行前の検証フックを設定しているか | パターン4 |
| フック | 実行後の監査ログを記録しているか | パターン4 |
| 全体設計 | 認可の粒度はツール単位ではなく操作単位になっているか | 全パターン |
| 全体設計 | 認可にTTL(有効期限)を設定しているか | 全パターン |
よくある落とし穴・注意点
落とし穴1: 「ツール単位の承認」で安全だと思い込む
「read_file の使用を許可しますか?」に対して「許可」を選んだ時点で、エージェントはプロジェクト内のどのファイルでも読めるようになります。これは read_file というツールの認可であって、「プロジェクト設定を読む」という操作の認可ではありません。
対策: 可能であれば、パラメータ制約付きの承認(per-invocation approval)を検討してください。Claude CodeのHooksのPreToolUseフックでパラメータを検証する方法が現実的です。
落とし穴2: 認可の永続化
一度許可した操作が、セッション全体を通じて有効であり続けることも問題です。セッション開始時に許可したツールが、数時間後の異なるコンテキストでも同じ権限で実行されます。
対策: 認可にTTL(有効期限)を設け、一定時間経過後は再承認を要求する仕組みを組み込みましょう。
落とし穴3: PostToolUse の監査を省略する
実行前の検証(PreToolUse)だけでは不十分です。実行結果が意図と異なるケースや、実行自体は正当でも結果が想定外であるケースがあります。
対策: PostToolUse フックで実行結果を監査し、異常を検知した場合はユーザーに通知する仕組みを設けましょう。
落とし穴4: 多層防御を忘れる
Execution Integrity は単独では銀の弾丸にはなりません。従来の認可制御(最小権限、スコープ制限)、入力検証(プロンプトインジェクション対策)、出力フィルタリングと組み合わせて初めて効果を発揮します。
まとめ
AIエージェントの「認可」は、従来のアクセス制御とは異なる構造的な課題を抱えています。
- Authorization-Execution Gap は、認可された操作が実行までの間に改ざん・逸脱されるリスクです
- この問題は、Rug Pull(ツール定義変更)、パラメータ改変、コンテキスト汚染、操作チェーンの悪用の4パターンで顕在化します
- 対策として、Execution Integrity(実行完全性)の検証を認可のライフサイクルに組み込みます
- 具体的には、ツール定義のハッシュ検証、パラメータバウンダリ、操作チェーン解析、Pre/Postフックの4つの設計パターンが有効です
AIエージェントが自律性を高めるほど、「認可したから安全」という前提は崩れていきます。「信頼するが、検証せよ(Trust, but verify)」——この原則を、認可の仕組みそのものに組み込むことが、安全なAIエージェント運用の鍵です。