flawopen.com/リファレンス/PR botのセキュリティ
AI PRレビューbot:Prompt InjectionとSSRF対策
AI PRレビューbot:Prompt InjectionによるSSRFを防ぐ
信頼できないPull Requestの文章がAIツールに与える影響と、botの権限やネットワークを制限する方法を解説します。
セキュリティリスク
CWE-1427 / CWE-918
AI・開発ワークフローのセキュリティ
やさしい説明:社内ネットワークの通行証を持つレビュー担当
Pull Requestはレビュー担当に届く荷物のようなものです。中身は役立つことがあっても、会社の方針になるわけではありません。AIレビューbotがプライベートネットワークから任意のURLを取得できると、誤解を招く指示がアクセス先に影響する可能性があります。リスクはツールとネットワークの権限次第です。敵対的な文章を読むだけでネットワーク権限が得られるわけではありません。
基本概念と重要用語
- 信頼できない入力:
- コントリビューターが送るPull Requestのタイトル、説明、コメント、diff、リンク先の内容。
- 間接Prompt Injection:
- 信頼できない文章がモデルのコンテキストに入ることでエージェントに影響すること。
- SSRFの境界:
- サーバー側の取得ツールが投稿者から直接アクセスできない宛先に到達すること。
- 影響の条件:
- アクセス可能な内部サービス、利用できる認証情報、不十分な認可または送信制御も必要です。
仕組みと根本原因
レビューbotは投稿者が制御できる文章と、信頼できるレビュー指示をモデルに渡すことがあります。モデルはどちらからでもツール呼び出しを提案できます。プロンプトや区切り文字はネットワークアクセスを認可しません。アプリがモデル選択の宛先を受け入れ、ネットワーク接続可能なツールがそこへ到達できる場合に限り、SSRFが成立します。クラウドメタデータや認証情報へのアクセスは追加の条件付き影響であり、Prompt Injectionだけで自動的に起こるわけではありません。
条件付きの攻撃経路
1
投稿者がPR内容を制御
外部投稿者はタイトル、説明、コメント、変更テキスト、リンク先に影響を与えられます。
2
エージェントが内容を読む
サービスが一部の未信頼フィールドをモデルのコンテキストへ入れると、回答やツール提案に影響する可能性があります。
3
ツールが到達範囲を決める
任意URLを取得できる場合、runnerはネットワークで許された宛先へ接続できます。そのツールがなければ、このSSRF経路はありません。
4
影響は環境で変わる
内部データや認証情報が露出するには、到達可能なサービス、メタデータ設定、権限、出力制御が許している必要があります。
コード:未検証の呼び出しと認可済み読み取り
無害なモック呼び出し
# INERT DEMO: model-selected URL reaches a mock tool without policy checks
from dataclasses import dataclass
@dataclass
class ToolCall:
name: str
arguments: dict[str, str]
class MockFetch:
def __init__(self):
self.calls = []
def __call__(self, url: str) -> str:
self.calls.append(url)
return "fixture response"
def dispatch_unchecked(call: ToolCall, tools: dict):
# The mock records a string; it makes no network request.
return tools[call.name](**call.arguments)
mock_fetch = MockFetch()
call = ToolCall("fetch_url", {"url": "https://docs.example.invalid/"})
assert dispatch_unchecked(call, {"fetch_url": mock_fetch}) == "fixture response"
assert mock_fetch.calls == ["https://docs.example.invalid/"]
ポリシーで制限した読み取りツール
# SAFE DEMO: the agent receives only a read-only PR capability
from dataclasses import dataclass
@dataclass(frozen=True)
class ReviewContext:
repository: str
pull_request: int
@dataclass
class ToolCall:
name: str
arguments: dict[str, object]
READ_ONLY_TOOLS = {"read_pr_diff"}
def dispatch_review_call(call: ToolCall, context: ReviewContext, policy, tools):
if call.name not in READ_ONLY_TOOLS:
raise PermissionError("Tool is not available to this review")
expected = {"repository": context.repository,
"pull_request": context.pull_request}
if call.arguments != expected:
raise ValueError("Arguments must match the authorized review")
if not policy.can_read_pr(context.repository, context.pull_request):
raise PermissionError("Review principal cannot read this pull request")
# The injected read_pr_diff function is trusted app code, not model output.
return tools["read_pr_diff"](**expected)
エンジニアリング強化チェックリスト
- このシナリオを確認済みのPR botインシデントとして説明しないでください。ここでは条件付きの設計リスクを示しています。
- diffを確認するだけのレビューbotに任意URL取得ツールを接続しないでください。
- 外部リンクのプレビューが必要なら、宛先allowlist、接続時のDNS/IP検証、リダイレクトごとの再検証、厳格な時間・サイズ制限、暗黙の認証情報なしを備えた専用egressサービスを使います。
- ネットワーク層でrunnerからプライベートネットワークとクラウドメタデータへのアクセスを遮断します。不要ならメタデータを無効にし、必要ならIMDSv2を必須にしてコンテナのhop limitを確認します。
- botにはリポジトリ限定の読み取り専用IDを付与します。未信頼PRのジョブに秘密情報を渡さず、特権ワークフローと外部コード実行を分離します。
- GitHub Actionsでは未信頼コードに最小権限の`pull_request`を優先します。`pull_request_target`は特権イベントとして扱い、その秘密情報や書き込みトークンでforkのコードを実行しません。
- 拒否したツール呼び出しと宛先判定を記録しますが、トークン、完全な非公開diff、機密レスポンス本文は保存しません。
関連する実在の脆弱性 — CVE-2023-32786
GitHubのレビュー済みCVE-2023-32786アドバイザリによると、LangChain 0.0.329より前ではPrompt Injectionによって任意URLを取得させ(SSRF)、後続タスクに影響する可能性がありました。修正版は0.0.329です。これは記録されたURL取得の連鎖であり、PR botのインシデントやクラウドメタデータ窃取を証明するものではありません。